diff options
204 files changed, 1907 insertions, 20736 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 2a60d1805..89f9c59d7 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -22,16 +22,16 @@ jobs: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup .NET - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 + uses: github/codeql-action/init@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 + uses: github/codeql-action/autobuild@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 + uses: github/codeql-action/analyze@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 62445a1ca..f872002c5 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-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@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: dotnet-version: '8.0.x' - name: Generate openapi.json @@ -53,7 +53,7 @@ jobs: 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 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: dotnet-version: '8.0.x' - name: Generate openapi.json @@ -78,12 +78,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 with: name: openapi-base path: openapi-base diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 3188828e1..5a0125f5f 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3 + - uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4 with: dotnet-version: ${{ env.SDK_VERSION }} diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index 926a7fbfb..5a1ca9f7a 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml index de093a988..d01b3f4a1 100644 --- a/.github/workflows/pull-request-stale.yaml +++ b/.github/workflows/pull-request-stale.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index fff7136b8..d208879d1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -172,6 +172,8 @@ - [sleepycatcoding](https://github.com/sleepycatcoding) - [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 @@ -242,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/Directory.Packages.props b/Directory.Packages.props index b0765c0de..ff76252f8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,9 +4,9 @@ </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" /> - <PackageVersion Include="AutoFixture" Version="4.18.0" /> + <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> + <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> + <PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" /> <PackageVersion Include="BlurHashSharp" Version="1.3.0" /> @@ -54,16 +54,16 @@ <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.1.0" /> + <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> - <PackageVersion Include="prometheus-net" Version="8.1.0" /> + <PackageVersion Include="prometheus-net" Version="8.2.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <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.Console" Version="5.0.1" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> - <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" /> + <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> <PackageVersion Include="SkiaSharp" Version="2.88.5" /> @@ -80,7 +80,7 @@ <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="TMDbLib" Version="2.1.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.3" /> diff --git a/Emby.Dlna/Common/Argument.cs b/Emby.Dlna/Common/Argument.cs deleted file mode 100644 index e4e9c55e0..000000000 --- a/Emby.Dlna/Common/Argument.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Emby.Dlna.Common -{ - /// <summary> - /// DLNA Query parameter type, used when querying DLNA devices via SOAP. - /// </summary> - public class Argument - { - /// <summary> - /// Gets or sets name of the DLNA argument. - /// </summary> - public string Name { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the direction of the parameter. - /// </summary> - public string Direction { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the related DLNA state variable for this argument. - /// </summary> - public string RelatedStateVariable { get; set; } = string.Empty; - } -} diff --git a/Emby.Dlna/Common/DeviceIcon.cs b/Emby.Dlna/Common/DeviceIcon.cs deleted file mode 100644 index f9fd1dcec..000000000 --- a/Emby.Dlna/Common/DeviceIcon.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Globalization; - -namespace Emby.Dlna.Common -{ - /// <summary> - /// Defines the <see cref="DeviceIcon" />. - /// </summary> - public class DeviceIcon - { - /// <summary> - /// Gets or sets the Url. - /// </summary> - public string Url { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the MimeType. - /// </summary> - public string MimeType { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the Width. - /// </summary> - public int Width { get; set; } - - /// <summary> - /// Gets or sets the Height. - /// </summary> - public int Height { get; set; } - - /// <summary> - /// Gets or sets the Depth. - /// </summary> - public string Depth { get; set; } = string.Empty; - - /// <inheritdoc /> - public override string ToString() - { - return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width); - } - } -} diff --git a/Emby.Dlna/Common/DeviceService.cs b/Emby.Dlna/Common/DeviceService.cs deleted file mode 100644 index c1369558e..000000000 --- a/Emby.Dlna/Common/DeviceService.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Emby.Dlna.Common -{ - /// <summary> - /// Defines the <see cref="DeviceService" />. - /// </summary> - public class DeviceService - { - /// <summary> - /// Gets or sets the Service Type. - /// </summary> - public string ServiceType { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the Service Id. - /// </summary> - public string ServiceId { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the Scpd Url. - /// </summary> - public string ScpdUrl { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the Control Url. - /// </summary> - public string ControlUrl { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the EventSubUrl. - /// </summary> - public string EventSubUrl { get; set; } = string.Empty; - - /// <inheritdoc /> - public override string ToString() => ServiceId; - } -} diff --git a/Emby.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs deleted file mode 100644 index 02b81a0aa..000000000 --- a/Emby.Dlna/Common/ServiceAction.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; - -namespace Emby.Dlna.Common -{ - /// <summary> - /// Defines the <see cref="ServiceAction" />. - /// </summary> - public class ServiceAction - { - /// <summary> - /// Initializes a new instance of the <see cref="ServiceAction"/> class. - /// </summary> - public ServiceAction() - { - ArgumentList = new List<Argument>(); - } - - /// <summary> - /// Gets or sets the name of the action. - /// </summary> - public string Name { get; set; } = string.Empty; - - /// <summary> - /// Gets the ArgumentList. - /// </summary> - public List<Argument> ArgumentList { get; } - - /// <inheritdoc /> - public override string ToString() => Name; - } -} diff --git a/Emby.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs deleted file mode 100644 index fd733e085..000000000 --- a/Emby.Dlna/Common/StateVariable.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Emby.Dlna.Common -{ - /// <summary> - /// Defines the <see cref="StateVariable" />. - /// </summary> - public class StateVariable - { - /// <summary> - /// Gets or sets the name of the state variable. - /// </summary> - public string Name { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the data type of the state variable. - /// </summary> - public string DataType { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets a value indicating whether it sends events. - /// </summary> - public bool SendsEvents { get; set; } - - /// <summary> - /// Gets or sets the allowed values range. - /// </summary> - public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>(); - - /// <inheritdoc /> - public override string ToString() => Name; - } -} diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs deleted file mode 100644 index f233468de..000000000 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ /dev/null @@ -1,92 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna.Configuration -{ - /// <summary> - /// The DlnaOptions class contains the user definable parameters for the dlna subsystems. - /// </summary> - public class DlnaOptions - { - /// <summary> - /// Initializes a new instance of the <see cref="DlnaOptions"/> class. - /// </summary> - public DlnaOptions() - { - EnablePlayTo = true; - EnableServer = false; - BlastAliveMessages = true; - SendOnlyMatchedHost = true; - ClientDiscoveryIntervalSeconds = 60; - AliveMessageIntervalSeconds = 180; - } - - /// <summary> - /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem. - /// </summary> - public bool EnablePlayTo { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem. - /// </summary> - public bool EnableServer { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log. - /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. - /// </summary> - public bool EnableDebugLog { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log. - /// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work. - /// </summary> - public bool EnablePlayToTracing { get; set; } - - /// <summary> - /// Gets or sets the ssdp client discovery interval time (in seconds). - /// This is the time after which the server will send a ssdp search request. - /// </summary> - public int ClientDiscoveryIntervalSeconds { get; set; } - - /// <summary> - /// Gets or sets the frequency at which ssdp alive notifications are transmitted. - /// </summary> - public int AliveMessageIntervalSeconds { get; set; } - - /// <summary> - /// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED. - /// </summary> - public int BlastAliveMessageIntervalSeconds - { - get - { - return AliveMessageIntervalSeconds; - } - - set - { - AliveMessageIntervalSeconds = value; - } - } - - /// <summary> - /// Gets or sets the default user account that the dlna server uses. - /// </summary> - public string? DefaultUserId { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether playTo device profiles should be created. - /// </summary> - public bool AutoCreatePlayToProfiles { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether to blast alive messages. - /// </summary> - public bool BlastAliveMessages { get; set; } = true; - - /// <summary> - /// gets or sets a value indicating whether to send only matched host. - /// </summary> - public bool SendOnlyMatchedHost { get; set; } = true; - } -} diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs deleted file mode 100644 index 3ca43052a..000000000 --- a/Emby.Dlna/ConfigurationExtension.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 - -using Emby.Dlna.Configuration; -using MediaBrowser.Common.Configuration; - -namespace Emby.Dlna -{ - public static class ConfigurationExtension - { - public static DlnaOptions GetDlnaConfiguration(this IConfigurationManager manager) - { - return manager.GetConfiguration<DlnaOptions>("dlna"); - } - } -} diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs deleted file mode 100644 index 916044a0c..000000000 --- a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs +++ /dev/null @@ -1,53 +0,0 @@ -#pragma warning disable CS1591 - -using System.Net.Http; -using System.Threading.Tasks; -using Emby.Dlna.Service; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.ConnectionManager -{ - /// <summary> - /// Defines the <see cref="ConnectionManagerService" />. - /// </summary> - public class ConnectionManagerService : BaseService, IConnectionManager - { - private readonly IDlnaManager _dlna; - private readonly IServerConfigurationManager _config; - - /// <summary> - /// Initializes a new instance of the <see cref="ConnectionManagerService"/> class. - /// </summary> - /// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param> - /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param> - /// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param> - /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param> - public ConnectionManagerService( - IDlnaManager dlna, - IServerConfigurationManager config, - ILogger<ConnectionManagerService> logger, - IHttpClientFactory httpClientFactory) - : base(logger, httpClientFactory) - { - _dlna = dlna; - _config = config; - } - - /// <inheritdoc /> - public string GetServiceXml() - { - return ConnectionManagerXmlBuilder.GetXml(); - } - - /// <inheritdoc /> - public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) - { - var profile = _dlna.GetProfile(request.Headers) ?? - _dlna.GetDefaultProfile(); - - return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request); - } - } -} diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs deleted file mode 100644 index db1190ae7..000000000 --- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs +++ /dev/null @@ -1,119 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using Emby.Dlna.Common; -using Emby.Dlna.Service; - -namespace Emby.Dlna.ConnectionManager -{ - /// <summary> - /// Defines the <see cref="ConnectionManagerXmlBuilder" />. - /// </summary> - public static class ConnectionManagerXmlBuilder - { - /// <summary> - /// Gets the ConnectionManager:1 service template. - /// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf. - /// </summary> - /// <returns>An XML description of this service.</returns> - public static string GetXml() - { - return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); - } - - /// <summary> - /// Get the list of state variables for this invocation. - /// </summary> - /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> - private static IEnumerable<StateVariable> GetStateVariables() - { - return new StateVariable[] - { - new StateVariable - { - Name = "SourceProtocolInfo", - DataType = "string", - SendsEvents = true - }, - - new StateVariable - { - Name = "SinkProtocolInfo", - DataType = "string", - SendsEvents = true - }, - - new StateVariable - { - Name = "CurrentConnectionIDs", - DataType = "string", - SendsEvents = true - }, - - new StateVariable - { - Name = "A_ARG_TYPE_ConnectionStatus", - DataType = "string", - SendsEvents = false, - - AllowedValues = new[] - { - "OK", - "ContentFormatMismatch", - "InsufficientBandwidth", - "UnreliableChannel", - "Unknown" - } - }, - - new StateVariable - { - Name = "A_ARG_TYPE_ConnectionManager", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_Direction", - DataType = "string", - SendsEvents = false, - - AllowedValues = new[] - { - "Output", - "Input" - } - }, - - new StateVariable - { - Name = "A_ARG_TYPE_ProtocolInfo", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_ConnectionID", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_AVTransportID", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_RcsID", - DataType = "ui4", - SendsEvents = false - } - }; - } - } -} diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs deleted file mode 100644 index 1a1790ee6..000000000 --- a/Emby.Dlna/ConnectionManager/ControlHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Xml; -using Emby.Dlna.Service; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Dlna; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.ConnectionManager -{ - /// <summary> - /// Defines the <see cref="ControlHandler" />. - /// </summary> - public class ControlHandler : BaseControlHandler - { - private readonly DeviceProfile _profile; - - /// <summary> - /// Initializes a new instance of the <see cref="ControlHandler"/> class. - /// </summary> - /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param> - public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) - : base(config, logger) - { - _profile = profile; - } - - /// <inheritdoc /> - protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) - { - if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) - { - HandleGetProtocolInfo(xmlWriter); - return; - } - - throw new ResourceNotFoundException("Unexpected control request name: " + methodName); - } - - /// <summary> - /// Builds the response to the GetProtocolInfo request. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private void HandleGetProtocolInfo(XmlWriter xmlWriter) - { - xmlWriter.WriteElementString("Source", _profile.ProtocolInfo); - xmlWriter.WriteElementString("Sink", string.Empty); - } - } -} diff --git a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs deleted file mode 100644 index 542c7bfb4..000000000 --- a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs +++ /dev/null @@ -1,234 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using Emby.Dlna.Common; - -namespace Emby.Dlna.ConnectionManager -{ - /// <summary> - /// Defines the <see cref="ServiceActionListBuilder" />. - /// </summary> - public static class ServiceActionListBuilder - { - /// <summary> - /// Returns an enumerable of the ConnectionManagar:1 DLNA actions. - /// </summary> - /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns> - public static IEnumerable<ServiceAction> GetActions() - { - var list = new List<ServiceAction> - { - GetCurrentConnectionInfo(), - GetProtocolInfo(), - GetCurrentConnectionIDs(), - ConnectionComplete(), - PrepareForConnection() - }; - - return list; - } - - /// <summary> - /// Returns the action details for "PrepareForConnection". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction PrepareForConnection() - { - var action = new ServiceAction - { - Name = "PrepareForConnection" - }; - - action.ArgumentList.Add(new Argument - { - Name = "RemoteProtocolInfo", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo" - }); - - action.ArgumentList.Add(new Argument - { - Name = "PeerConnectionManager", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ConnectionManager" - }); - - action.ArgumentList.Add(new Argument - { - Name = "PeerConnectionID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ConnectionID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Direction", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Direction" - }); - - action.ArgumentList.Add(new Argument - { - Name = "ConnectionID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_ConnectionID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "AVTransportID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_AVTransportID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "RcsID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_RcsID" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetCurrentConnectionInfo". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetCurrentConnectionInfo() - { - var action = new ServiceAction - { - Name = "GetCurrentConnectionInfo" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ConnectionID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ConnectionID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "RcsID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_RcsID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "AVTransportID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_AVTransportID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "ProtocolInfo", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo" - }); - - action.ArgumentList.Add(new Argument - { - Name = "PeerConnectionManager", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_ConnectionManager" - }); - - action.ArgumentList.Add(new Argument - { - Name = "PeerConnectionID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_ConnectionID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Direction", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Direction" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Status", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_ConnectionStatus" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetProtocolInfo". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetProtocolInfo() - { - var action = new ServiceAction - { - Name = "GetProtocolInfo" - }; - - action.ArgumentList.Add(new Argument - { - Name = "Source", - Direction = "out", - RelatedStateVariable = "SourceProtocolInfo" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Sink", - Direction = "out", - RelatedStateVariable = "SinkProtocolInfo" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetCurrentConnectionIDs". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetCurrentConnectionIDs() - { - var action = new ServiceAction - { - Name = "GetCurrentConnectionIDs" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ConnectionIDs", - Direction = "out", - RelatedStateVariable = "CurrentConnectionIDs" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "ConnectionComplete". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction ConnectionComplete() - { - var action = new ServiceAction - { - Name = "ConnectionComplete" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ConnectionID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ConnectionID" - }); - - return action; - } - } -} diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs deleted file mode 100644 index 389e971a6..000000000 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs +++ /dev/null @@ -1,173 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Emby.Dlna.Service; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Globalization; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.ContentDirectory -{ - /// <summary> - /// Defines the <see cref="ContentDirectoryService" />. - /// </summary> - public class ContentDirectoryService : BaseService, IContentDirectory - { - private readonly ILibraryManager _libraryManager; - private readonly IImageProcessor _imageProcessor; - private readonly IUserDataManager _userDataManager; - private readonly IDlnaManager _dlna; - private readonly IServerConfigurationManager _config; - private readonly IUserManager _userManager; - private readonly ILocalizationManager _localization; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IUserViewManager _userViewManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly ITVSeriesManager _tvSeriesManager; - - /// <summary> - /// Initializes a new instance of the <see cref="ContentDirectoryService"/> class. - /// </summary> - /// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - /// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> - public ContentDirectoryService( - IDlnaManager dlna, - IUserDataManager userDataManager, - IImageProcessor imageProcessor, - ILibraryManager libraryManager, - IServerConfigurationManager config, - IUserManager userManager, - ILogger<ContentDirectoryService> logger, - IHttpClientFactory httpClient, - ILocalizationManager localization, - IMediaSourceManager mediaSourceManager, - IUserViewManager userViewManager, - IMediaEncoder mediaEncoder, - ITVSeriesManager tvSeriesManager) - : base(logger, httpClient) - { - _dlna = dlna; - _userDataManager = userDataManager; - _imageProcessor = imageProcessor; - _libraryManager = libraryManager; - _config = config; - _userManager = userManager; - _localization = localization; - _mediaSourceManager = mediaSourceManager; - _userViewManager = userViewManager; - _mediaEncoder = mediaEncoder; - _tvSeriesManager = tvSeriesManager; - } - - /// <summary> - /// Gets the system id. (A unique id which changes on when our definition changes.) - /// </summary> - private static int SystemUpdateId - { - get - { - var now = DateTime.UtcNow; - - return now.Year + now.DayOfYear + now.Hour; - } - } - - /// <inheritdoc /> - public string GetServiceXml() - { - return ContentDirectoryXmlBuilder.GetXml(); - } - - /// <inheritdoc /> - public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) - { - ArgumentNullException.ThrowIfNull(request); - - var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); - - var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); - - var user = GetUser(profile); - - return new ControlHandler( - Logger, - _libraryManager, - profile, - serverAddress, - null, - _imageProcessor, - _userDataManager, - user, - SystemUpdateId, - _config, - _localization, - _mediaSourceManager, - _userViewManager, - _mediaEncoder, - _tvSeriesManager) - .ProcessControlRequestAsync(request); - } - - /// <summary> - /// Get the user stored in the device profile. - /// </summary> - /// <param name="profile">The <see cref="DeviceProfile"/>.</param> - /// <returns>The <see cref="User"/>.</returns> - private User? GetUser(DeviceProfile profile) - { - if (!string.IsNullOrEmpty(profile.UserId)) - { - var user = _userManager.GetUserById(Guid.Parse(profile.UserId)); - - if (user is not null) - { - return user; - } - } - - var userId = _config.GetDlnaConfiguration().DefaultUserId; - - if (!string.IsNullOrEmpty(userId)) - { - var user = _userManager.GetUserById(Guid.Parse(userId)); - - if (user is not null) - { - return user; - } - } - - foreach (var user in _userManager.Users) - { - if (user.HasPermission(PermissionKind.IsAdministrator)) - { - return user; - } - } - - return _userManager.Users.FirstOrDefault(); - } - } -} diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs deleted file mode 100644 index 9af28aa7c..000000000 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs +++ /dev/null @@ -1,159 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using Emby.Dlna.Common; -using Emby.Dlna.Service; - -namespace Emby.Dlna.ContentDirectory -{ - /// <summary> - /// Defines the <see cref="ContentDirectoryXmlBuilder" />. - /// </summary> - public static class ContentDirectoryXmlBuilder - { - /// <summary> - /// Gets the ContentDirectory:1 service template. - /// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf. - /// </summary> - /// <returns>An XML description of this service.</returns> - public static string GetXml() - { - return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); - } - - /// <summary> - /// Get the list of state variables for this invocation. - /// </summary> - /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> - private static IEnumerable<StateVariable> GetStateVariables() - { - return new StateVariable[] - { - new StateVariable - { - Name = "A_ARG_TYPE_Filter", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_SortCriteria", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_Index", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_Count", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_UpdateID", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "SearchCapabilities", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "SortCapabilities", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "SystemUpdateID", - DataType = "ui4", - SendsEvents = true - }, - - new StateVariable - { - Name = "A_ARG_TYPE_SearchCriteria", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_Result", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_ObjectID", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_BrowseFlag", - DataType = "string", - SendsEvents = false, - - AllowedValues = new[] - { - "BrowseMetadata", - "BrowseDirectChildren" - } - }, - - new StateVariable - { - Name = "A_ARG_TYPE_BrowseLetter", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_CategoryType", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_RID", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_PosSec", - DataType = "ui4", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_Featurelist", - DataType = "string", - SendsEvents = false - } - }; - } - } -} diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs deleted file mode 100644 index 99068826d..000000000 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ /dev/null @@ -1,1250 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Xml; -using Emby.Dlna.Didl; -using Emby.Dlna.Service; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; -using Genre = MediaBrowser.Controller.Entities.Genre; - -namespace Emby.Dlna.ContentDirectory -{ - /// <summary> - /// Defines the <see cref="ControlHandler" />. - /// </summary> - public class ControlHandler : BaseControlHandler - { - private const string NsDc = "http://purl.org/dc/elements/1.1/"; - private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; - private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - - private readonly ILibraryManager _libraryManager; - private readonly IUserDataManager _userDataManager; - private readonly User _user; - private readonly IUserViewManager _userViewManager; - private readonly ITVSeriesManager _tvSeriesManager; - - private readonly int _systemUpdateId; - - private readonly DidlBuilder _didlBuilder; - - private readonly DeviceProfile _profile; - - /// <summary> - /// Initializes a new instance of the <see cref="ControlHandler"/> class. - /// </summary> - /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="libraryManager">The <see cref="ILibraryManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="serverAddress">The server address to use in this instance> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="accessToken">The <see cref="string"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="imageProcessor">The <see cref="IImageProcessor"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="userDataManager">The <see cref="IUserDataManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="user">The <see cref="User"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="systemUpdateId">The system id for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="localization">The <see cref="ILocalizationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="userViewManager">The <see cref="IUserViewManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - public ControlHandler( - ILogger logger, - ILibraryManager libraryManager, - DeviceProfile profile, - string serverAddress, - string accessToken, - IImageProcessor imageProcessor, - IUserDataManager userDataManager, - User user, - int systemUpdateId, - IServerConfigurationManager config, - ILocalizationManager localization, - IMediaSourceManager mediaSourceManager, - IUserViewManager userViewManager, - IMediaEncoder mediaEncoder, - ITVSeriesManager tvSeriesManager) - : base(config, logger) - { - _libraryManager = libraryManager; - _userDataManager = userDataManager; - _user = user; - _systemUpdateId = systemUpdateId; - _userViewManager = userViewManager; - _tvSeriesManager = tvSeriesManager; - _profile = profile; - - _didlBuilder = new DidlBuilder( - profile, - user, - imageProcessor, - serverAddress, - accessToken, - userDataManager, - localization, - mediaSourceManager, - Logger, - mediaEncoder, - libraryManager); - } - - /// <inheritdoc /> - protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) - { - ArgumentNullException.ThrowIfNull(xmlWriter); - - ArgumentNullException.ThrowIfNull(methodParams); - - const string DeviceId = "test"; - - if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase)) - { - HandleGetSearchCapabilities(xmlWriter); - return; - } - - if (string.Equals(methodName, "GetSortCapabilities", StringComparison.OrdinalIgnoreCase)) - { - HandleGetSortCapabilities(xmlWriter); - return; - } - - if (string.Equals(methodName, "GetSortExtensionCapabilities", StringComparison.OrdinalIgnoreCase)) - { - HandleGetSortExtensionCapabilities(xmlWriter); - return; - } - - if (string.Equals(methodName, "GetSystemUpdateID", StringComparison.OrdinalIgnoreCase)) - { - HandleGetSystemUpdateID(xmlWriter); - return; - } - - if (string.Equals(methodName, "Browse", StringComparison.OrdinalIgnoreCase)) - { - HandleBrowse(xmlWriter, methodParams, DeviceId); - return; - } - - if (string.Equals(methodName, "X_GetFeatureList", StringComparison.OrdinalIgnoreCase)) - { - HandleXGetFeatureList(xmlWriter); - return; - } - - if (string.Equals(methodName, "GetFeatureList", StringComparison.OrdinalIgnoreCase)) - { - HandleGetFeatureList(xmlWriter); - return; - } - - if (string.Equals(methodName, "X_SetBookmark", StringComparison.OrdinalIgnoreCase)) - { - HandleXSetBookmark(methodParams); - return; - } - - if (string.Equals(methodName, "Search", StringComparison.OrdinalIgnoreCase)) - { - HandleSearch(xmlWriter, methodParams, DeviceId); - return; - } - - if (string.Equals(methodName, "X_BrowseByLetter", StringComparison.OrdinalIgnoreCase)) - { - HandleXBrowseByLetter(xmlWriter, methodParams, DeviceId); - return; - } - - throw new ResourceNotFoundException("Unexpected control request name: " + methodName); - } - - /// <summary> - /// Adds a "XSetBookmark" element to the xml document. - /// </summary> - /// <param name="sparams">The method parameters.</param> - private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams) - { - var id = sparams["ObjectID"]; - - var serverItem = GetItemFromObjectId(id); - - var item = serverItem.Item; - - var newbookmark = int.Parse(sparams["PosSecond"], CultureInfo.InvariantCulture); - - var userdata = _userDataManager.GetUserData(_user, item); - - userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks; - - _userDataManager.SaveUserData( - _user, - item, - userdata, - UserDataSaveReason.TogglePlayed, - CancellationToken.None); - } - - /// <summary> - /// Adds the "SearchCaps" element to the xml document. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private static void HandleGetSearchCapabilities(XmlWriter xmlWriter) - { - xmlWriter.WriteElementString( - "SearchCaps", - "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords"); - } - - /// <summary> - /// Adds the "SortCaps" element to the xml document. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private static void HandleGetSortCapabilities(XmlWriter xmlWriter) - { - xmlWriter.WriteElementString( - "SortCaps", - "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating"); - } - - /// <summary> - /// Adds the "SortExtensionCaps" element to the xml document. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private static void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter) - { - xmlWriter.WriteElementString( - "SortExtensionCaps", - "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating"); - } - - /// <summary> - /// Adds the "Id" element to the xml document. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private void HandleGetSystemUpdateID(XmlWriter xmlWriter) - { - xmlWriter.WriteElementString("Id", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); - } - - /// <summary> - /// Adds the "FeatureList" element to the xml document. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private static void HandleGetFeatureList(XmlWriter xmlWriter) - { - xmlWriter.WriteElementString("FeatureList", WriteFeatureListXml()); - } - - /// <summary> - /// Adds the "FeatureList" element to the xml document. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private static void HandleXGetFeatureList(XmlWriter xmlWriter) - => HandleGetFeatureList(xmlWriter); - - /// <summary> - /// Builds a static feature list. - /// </summary> - /// <returns>The xml feature list.</returns> - private static string WriteFeatureListXml() - { - return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" - + "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">" - + "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">" - + "<container id=\"0\" type=\"object.item.imageItem\"/>" - + "<container id=\"0\" type=\"object.item.audioItem\"/>" - + "<container id=\"0\" type=\"object.item.videoItem\"/>" - + "</Feature>" - + "</Features>"; - } - - /// <summary> - /// Builds the "Browse" xml response. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - /// <param name="sparams">The method parameters.</param> - /// <param name="deviceId">The device Id to use.</param> - private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) - { - var id = sparams["ObjectID"]; - var flag = sparams["BrowseFlag"]; - var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); - var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); - - var provided = 0; - - // Default to null instead of 0 - // Upnp inspector sends 0 as requestedCount when it wants everything - int? requestedCount = null; - int? start = 0; - - if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out var requestedVal) && requestedVal > 0) - { - requestedCount = requestedVal; - } - - if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out var startVal) && startVal > 0) - { - start = startVal; - } - - int totalCount; - - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false, - OmitXmlDeclaration = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) - using (var writer = XmlWriter.Create(builder, settings)) - { - writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - - writer.WriteAttributeString("xmlns", "dc", null, NsDc); - writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); - writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); - - DidlBuilder.WriteXmlRootAttributes(_profile, writer); - - var serverItem = GetItemFromObjectId(id); - var item = serverItem.Item; - - if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal)) - { - totalCount = 1; - - if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) - { - var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount); - - _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); - } - else - { - _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter); - } - - provided++; - } - else - { - var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount); - totalCount = childrenResult.TotalRecordCount; - - provided = childrenResult.Items.Count; - - foreach (var i in childrenResult.Items) - { - var childItem = i.Item; - var displayStubType = i.StubType; - - if (childItem.IsDisplayedAsFolder || displayStubType.HasValue) - { - var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0) - .TotalRecordCount; - - _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter); - } - else - { - _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter); - } - } - } - - writer.WriteFullEndElement(); - writer.Flush(); - xmlWriter.WriteElementString("Result", builder.ToString()); - } - - xmlWriter.WriteElementString("NumberReturned", provided.ToString(CultureInfo.InvariantCulture)); - xmlWriter.WriteElementString("TotalMatches", totalCount.ToString(CultureInfo.InvariantCulture)); - xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); - } - - /// <summary> - /// Builds the response to the "X_BrowseByLetter request. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - /// <param name="sparams">The method parameters.</param> - /// <param name="deviceId">The device id.</param> - private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) - { - // TODO: Implement this method - HandleSearch(xmlWriter, sparams, deviceId); - } - - /// <summary> - /// Builds a response to the "Search" request. - /// </summary> - /// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param> - /// <param name="sparams">The method parameters.</param> - /// <param name="deviceId">The deviceId<see cref="string"/>.</param> - private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) - { - var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty)); - var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); - var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); - - // sort example: dc:title, dc:date - - // Default to null instead of 0 - // Upnp inspector sends 0 as requestedCount when it wants everything - int? requestedCount = null; - int? start = 0; - - if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out var requestedVal) && requestedVal > 0) - { - requestedCount = requestedVal; - } - - if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out var startVal) && startVal > 0) - { - start = startVal; - } - - QueryResult<BaseItem> childrenResult; - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false, - OmitXmlDeclaration = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) - using (var writer = XmlWriter.Create(builder, settings)) - { - writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - writer.WriteAttributeString("xmlns", "dc", null, NsDc); - writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); - writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); - - DidlBuilder.WriteXmlRootAttributes(_profile, writer); - - var serverItem = GetItemFromObjectId(sparams["ContainerID"]); - - var item = serverItem.Item; - - childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount); - foreach (var i in childrenResult.Items) - { - if (i.IsDisplayedAsFolder) - { - var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0) - .TotalRecordCount; - - _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter); - } - else - { - _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter); - } - } - - writer.WriteFullEndElement(); - writer.Flush(); - xmlWriter.WriteElementString("Result", builder.ToString()); - } - - xmlWriter.WriteElementString("NumberReturned", childrenResult.Items.Count.ToString(CultureInfo.InvariantCulture)); - xmlWriter.WriteElementString("TotalMatches", childrenResult.TotalRecordCount.ToString(CultureInfo.InvariantCulture)); - xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); - } - - /// <summary> - /// Returns the child items meeting the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="search">The <see cref="SearchCriteria"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{BaseItem}"/>.</returns> - private static QueryResult<BaseItem> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) - { - var folder = (Folder)item; - - MediaType[] mediaTypes = Array.Empty<MediaType>(); - bool? isFolder = null; - - switch (search.SearchType) - { - case SearchType.Audio: - mediaTypes = new[] { MediaType.Audio }; - isFolder = false; - break; - case SearchType.Video: - mediaTypes = new[] { MediaType.Video }; - isFolder = false; - break; - case SearchType.Image: - mediaTypes = new[] { MediaType.Photo }; - isFolder = false; - break; - case SearchType.Playlist: - case SearchType.MusicAlbum: - isFolder = true; - break; - } - - return folder.GetItems(new InternalItemsQuery - { - Limit = limit, - StartIndex = startIndex, - OrderBy = GetOrderBy(sort, folder.IsPreSorted), - User = user, - Recursive = true, - IsMissing = false, - ExcludeItemTypes = new[] { BaseItemKind.Book }, - IsFolder = isFolder, - MediaTypes = mediaTypes, - DtoOptions = GetDtoOptions() - }); - } - - /// <summary> - /// Returns a new DtoOptions object. - /// </summary> - /// <returns>The <see cref="DtoOptions"/>.</returns> - private static DtoOptions GetDtoOptions() - { - return new DtoOptions(true); - } - - /// <summary> - /// Returns the User items meeting the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="stubType">The <see cref="StubType"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit) - { - switch (item) - { - case MusicGenre: - return GetMusicGenreItems(item, user, sort, startIndex, limit); - case MusicArtist: - return GetMusicArtistItems(item, user, sort, startIndex, limit); - case Genre: - return GetGenreItems(item, user, sort, startIndex, limit); - } - - if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder) - { - switch (collectionFolder.CollectionType) - { - 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); - } - } - - if (stubType.HasValue && stubType.Value != StubType.Folder) - { - // TODO should this be doing something? - return new QueryResult<ServerItem>(); - } - - var folder = (Folder)item; - - var query = new InternalItemsQuery(user) - { - Limit = limit, - StartIndex = startIndex, - IsVirtualItem = false, - ExcludeItemTypes = new[] { BaseItemKind.Book }, - IsPlaceHolder = false, - DtoOptions = GetDtoOptions(), - OrderBy = GetOrderBy(sort, folder.IsPreSorted) - }; - - var queryResult = folder.GetItems(query); - - return ToResult(startIndex, queryResult); - } - - /// <summary> - /// Returns the Live Tv Channels meeting the criteria. - /// </summary> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetLiveTvChannels(User user, SortCriteria sort, int? startIndex, int? limit) - { - var query = new InternalItemsQuery(user) - { - StartIndex = startIndex, - Limit = limit, - IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, - OrderBy = GetOrderBy(sort, false) - }; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(startIndex, result); - } - - /// <summary> - /// Returns the music folders meeting the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="stubType">The <see cref="StubType"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMusicFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) - { - var query = new InternalItemsQuery(user) - { - StartIndex = startIndex, - Limit = limit, - OrderBy = GetOrderBy(sort, false) - }; - - switch (stubType) - { - case StubType.Latest: - return GetLatest(item, query, BaseItemKind.Audio); - case StubType.Playlists: - return GetMusicPlaylists(query); - case StubType.Albums: - return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum); - case StubType.Artists: - return GetMusicArtists(item, query); - case StubType.AlbumArtists: - return GetMusicAlbumArtists(item, query); - case StubType.FavoriteAlbums: - return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true); - case StubType.FavoriteArtists: - return GetFavoriteArtists(item, query); - case StubType.FavoriteSongs: - return GetChildrenOfItem(item, query, BaseItemKind.Audio, true); - case StubType.Songs: - return GetChildrenOfItem(item, query, BaseItemKind.Audio); - case StubType.Genres: - return GetMusicGenres(item, query); - } - - var serverItems = new ServerItem[] - { - new(item, StubType.Latest), - new(item, StubType.Playlists), - new(item, StubType.Albums), - new(item, StubType.AlbumArtists), - new(item, StubType.Artists), - new(item, StubType.Songs), - new(item, StubType.Genres), - new(item, StubType.FavoriteArtists), - new(item, StubType.FavoriteAlbums), - new(item, StubType.FavoriteSongs) - }; - - if (limit < serverItems.Length) - { - serverItems = serverItems[..limit.Value]; - } - - return new QueryResult<ServerItem>( - startIndex, - serverItems.Length, - serverItems); - } - - /// <summary> - /// Returns the movie folders meeting the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="stubType">The <see cref="StubType"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMovieFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) - { - var query = new InternalItemsQuery(user) - { - StartIndex = startIndex, - Limit = limit, - OrderBy = GetOrderBy(sort, false) - }; - - switch (stubType) - { - case StubType.ContinueWatching: - return GetMovieContinueWatching(item, query); - case StubType.Latest: - return GetLatest(item, query, BaseItemKind.Movie); - case StubType.Movies: - return GetChildrenOfItem(item, query, BaseItemKind.Movie); - case StubType.Collections: - return GetMovieCollections(query); - case StubType.Favorites: - return GetChildrenOfItem(item, query, BaseItemKind.Movie, true); - case StubType.Genres: - return GetGenres(item, query); - } - - var array = new ServerItem[] - { - new(item, StubType.ContinueWatching), - new(item, StubType.Latest), - new(item, StubType.Movies), - new(item, StubType.Collections), - new(item, StubType.Favorites), - new(item, StubType.Genres) - }; - - if (limit < array.Length) - { - array = array[..limit.Value]; - } - - return new QueryResult<ServerItem>( - startIndex, - array.Length, - array); - } - - /// <summary> - /// Returns the folders meeting the criteria. - /// </summary> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetFolders(User user, int? startIndex, int? limit) - { - var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true); - var totalRecordCount = folders.Count; - // Handle paging - var items = folders - .OrderBy(i => i.SortName) - .Skip(startIndex ?? 0) - .Take(limit ?? int.MaxValue) - .Select(i => new ServerItem(i, StubType.Folder)) - .ToArray(); - - return new QueryResult<ServerItem>( - startIndex, - totalRecordCount, - items); - } - - /// <summary> - /// Returns the TV folders meeting the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="stubType">The <see cref="StubType"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) - { - var query = new InternalItemsQuery(user) - { - StartIndex = startIndex, - Limit = limit, - OrderBy = GetOrderBy(sort, false) - }; - - switch (stubType) - { - case StubType.ContinueWatching: - return GetMovieContinueWatching(item, query); - case StubType.NextUp: - return GetNextUp(item, query); - case StubType.Latest: - return GetLatest(item, query, BaseItemKind.Episode); - case StubType.Series: - return GetChildrenOfItem(item, query, BaseItemKind.Series); - case StubType.FavoriteSeries: - return GetChildrenOfItem(item, query, BaseItemKind.Series, true); - case StubType.FavoriteEpisodes: - return GetChildrenOfItem(item, query, BaseItemKind.Episode, true); - case StubType.Genres: - return GetGenres(item, query); - } - - var serverItems = new ServerItem[] - { - new(item, StubType.ContinueWatching), - new(item, StubType.NextUp), - new(item, StubType.Latest), - new(item, StubType.Series), - new(item, StubType.FavoriteSeries), - new(item, StubType.FavoriteEpisodes), - new(item, StubType.Genres) - }; - - if (limit < serverItems.Length) - { - serverItems = serverItems[..limit.Value]; - } - - return new QueryResult<ServerItem>( - startIndex, - serverItems.Length, - serverItems); - } - - /// <summary> - /// Returns the Movies that are part watched that meet the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMovieContinueWatching(BaseItem parent, InternalItemsQuery query) - { - query.Recursive = true; - query.Parent = parent; - - query.OrderBy = new[] - { - (ItemSortBy.DatePlayed, SortOrder.Descending), - (ItemSortBy.SortName, SortOrder.Ascending) - }; - - query.IsResumable = true; - query.Limit ??= 10; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(query.StartIndex, result); - } - - /// <summary> - /// Returns the Movie collections meeting the criteria. - /// </summary> - /// <param name="query">The see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query) - { - query.Recursive = true; - query.IncludeItemTypes = new[] { BaseItemKind.BoxSet }; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(query.StartIndex, result); - } - - /// <summary> - /// Returns the children that meet the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <param name="itemType">The item type.</param> - /// <param name="isFavorite">A value indicating whether to only fetch favorite items.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false) - { - query.Recursive = true; - query.Parent = parent; - query.IsFavorite = isFavorite; - query.IncludeItemTypes = new[] { itemType }; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(query.StartIndex, result); - } - - /// <summary> - /// Returns the genres meeting the criteria. - /// The GetGenres. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetGenres(BaseItem parent, InternalItemsQuery query) - { - // Don't sort - query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); - query.AncestorIds = new[] { parent.Id }; - var genresResult = _libraryManager.GetGenres(query); - - return ToResult(query.StartIndex, genresResult); - } - - /// <summary> - /// Returns the music genres meeting the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, InternalItemsQuery query) - { - // Don't sort - query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); - query.AncestorIds = new[] { parent.Id }; - var genresResult = _libraryManager.GetMusicGenres(query); - - return ToResult(query.StartIndex, genresResult); - } - - /// <summary> - /// Returns the music albums by artist that meet the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query) - { - // Don't sort - query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); - query.AncestorIds = new[] { parent.Id }; - var artists = _libraryManager.GetAlbumArtists(query); - - return ToResult(query.StartIndex, artists); - } - - /// <summary> - /// Returns the music artists meeting the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, InternalItemsQuery query) - { - // Don't sort - query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); - query.AncestorIds = new[] { parent.Id }; - var artists = _libraryManager.GetArtists(query); - return ToResult(query.StartIndex, artists); - } - - /// <summary> - /// Returns the artists tagged as favourite that meet the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, InternalItemsQuery query) - { - // Don't sort - query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); - query.AncestorIds = new[] { parent.Id }; - query.IsFavorite = true; - var artists = _libraryManager.GetArtists(query); - return ToResult(query.StartIndex, artists); - } - - /// <summary> - /// Returns the music playlists meeting the criteria. - /// </summary> - /// <param name="query">The query<see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query) - { - query.Parent = null; - query.IncludeItemTypes = new[] { BaseItemKind.Playlist }; - query.Recursive = true; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(query.StartIndex, result); - } - - /// <summary> - /// Returns the next up item meeting the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetNextUp(BaseItem parent, InternalItemsQuery query) - { - query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); - - var result = _tvSeriesManager.GetNextUp( - new NextUpQuery - { - Limit = query.Limit, - StartIndex = query.StartIndex, - // User cannot be null here as the caller has set it - UserId = query.User!.Id - }, - new[] { parent }, - query.DtoOptions); - - return ToResult(query.StartIndex, result); - } - - /// <summary> - /// Returns the latest items of [itemType] meeting the criteria. - /// </summary> - /// <param name="parent">The <see cref="BaseItem"/>.</param> - /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> - /// <param name="itemType">The item type.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType) - { - query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); - - var items = _userViewManager.GetLatestItems( - new LatestItemsQuery - { - // User cannot be null here as the caller has set it - UserId = query.User!.Id, - Limit = query.Limit ?? 50, - IncludeItemTypes = new[] { itemType }, - ParentId = parent?.Id ?? Guid.Empty, - GroupItems = true - }, - query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i is not null).ToArray(); - - return ToResult(query.StartIndex, items); - } - - /// <summary> - /// Returns music artist items that meet the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMusicArtistItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit) - { - var query = new InternalItemsQuery(user) - { - Recursive = true, - ArtistIds = new[] { item.Id }, - IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, - Limit = limit, - StartIndex = startIndex, - DtoOptions = GetDtoOptions(), - OrderBy = GetOrderBy(sort, false) - }; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(startIndex, result); - } - - /// <summary> - /// Returns the genre items meeting the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit) - { - var query = new InternalItemsQuery(user) - { - Recursive = true, - GenreIds = new[] { item.Id }, - IncludeItemTypes = new[] - { - BaseItemKind.Movie, - BaseItemKind.Series - }, - Limit = limit, - StartIndex = startIndex, - DtoOptions = GetDtoOptions(), - OrderBy = GetOrderBy(sort, false) - }; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(startIndex, result); - } - - /// <summary> - /// Returns the music genre items meeting the criteria. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="user">The <see cref="User"/>.</param> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="startIndex">The start index.</param> - /// <param name="limit">The maximum number to return.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private QueryResult<ServerItem> GetMusicGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit) - { - var query = new InternalItemsQuery(user) - { - Recursive = true, - GenreIds = new[] { item.Id }, - IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, - Limit = limit, - StartIndex = startIndex, - DtoOptions = GetDtoOptions(), - OrderBy = GetOrderBy(sort, false) - }; - - var result = _libraryManager.GetItemsResult(query); - - return ToResult(startIndex, result); - } - - /// <summary> - /// Converts <see cref="IReadOnlyCollection{BaseItem}"/> into a <see cref="QueryResult{ServerItem}"/>. - /// </summary> - /// <param name="startIndex">The start index.</param> - /// <param name="result">An array of <see cref="BaseItem"/>.</param> - /// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns> - private static QueryResult<ServerItem> ToResult(int? startIndex, IReadOnlyCollection<BaseItem> result) - { - var serverItems = result - .Select(i => new ServerItem(i, null)) - .ToArray(); - - return new QueryResult<ServerItem>( - startIndex, - result.Count, - serverItems); - } - - /// <summary> - /// Converts a <see cref="QueryResult{BaseItem}"/> to a <see cref="QueryResult{ServerItem}"/>. - /// </summary> - /// <param name="startIndex">The index the result started at.</param> - /// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private static QueryResult<ServerItem> ToResult(int? startIndex, QueryResult<BaseItem> result) - { - var length = result.Items.Count; - var serverItems = new ServerItem[length]; - for (var i = 0; i < length; i++) - { - serverItems[i] = new ServerItem(result.Items[i], null); - } - - return new QueryResult<ServerItem>( - startIndex, - result.TotalRecordCount, - serverItems); - } - - /// <summary> - /// Converts a query result to a <see cref="QueryResult{ServerItem}"/>. - /// </summary> - /// <param name="startIndex">The start index.</param> - /// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param> - /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> - private static QueryResult<ServerItem> ToResult(int? startIndex, QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result) - { - var length = result.Items.Count; - var serverItems = new ServerItem[length]; - for (var i = 0; i < length; i++) - { - serverItems[i] = new ServerItem(result.Items[i].Item, null); - } - - return new QueryResult<ServerItem>( - startIndex, - result.TotalRecordCount, - serverItems); - } - - /// <summary> - /// Gets the sorting method on a query. - /// </summary> - /// <param name="sort">The <see cref="SortCriteria"/>.</param> - /// <param name="isPreSorted">True if pre-sorted.</param> - private static (ItemSortBy SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted) - { - return isPreSorted ? Array.Empty<(ItemSortBy, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) }; - } - - /// <summary> - /// Retrieves the ServerItem id. - /// </summary> - /// <param name="id">The id<see cref="string"/>.</param> - /// <returns>The <see cref="ServerItem"/>.</returns> - private ServerItem GetItemFromObjectId(string id) - { - return DidlBuilder.IsIdRoot(id) - ? new ServerItem(_libraryManager.GetUserRootFolder(), null) - : ParseItemId(id); - } - - /// <summary> - /// Parses the item id into a <see cref="ServerItem"/>. - /// </summary> - /// <param name="id">The <see cref="string"/>.</param> - /// <returns>The corresponding <see cref="ServerItem"/>.</returns> - private ServerItem ParseItemId(string id) - { - StubType? stubType = null; - - // After using PlayTo, MediaMonkey sends a request to the server trying to get item info - const string ParamsSrch = "Params="; - var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase); - if (paramsIndex != -1) - { - id = id[(paramsIndex + ParamsSrch.Length)..]; - - var parts = id.Split(';'); - id = parts[23]; - } - - var dividerIndex = id.IndexOf('_', StringComparison.Ordinal); - if (dividerIndex != -1 && Enum.TryParse<StubType>(id.AsSpan(0, dividerIndex), true, out var parsedStubType)) - { - id = id[(dividerIndex + 1)..]; - stubType = parsedStubType; - } - - if (Guid.TryParse(id, out var itemId)) - { - var item = _libraryManager.GetItemById(itemId); - - return new ServerItem(item, stubType); - } - - Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id); - - return new ServerItem(_libraryManager.GetUserRootFolder(), null); - } - } -} diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs deleted file mode 100644 index df05fa966..000000000 --- a/Emby.Dlna/ContentDirectory/ServerItem.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediaBrowser.Controller.Entities; - -namespace Emby.Dlna.ContentDirectory -{ - /// <summary> - /// Defines the <see cref="ServerItem" />. - /// </summary> - internal class ServerItem - { - /// <summary> - /// Initializes a new instance of the <see cref="ServerItem"/> class. - /// </summary> - /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="stubType">The stub type.</param> - public ServerItem(BaseItem item, StubType? stubType) - { - Item = item; - - if (stubType.HasValue) - { - StubType = stubType; - } - else if (item is IItemByName and not Folder) - { - StubType = Dlna.ContentDirectory.StubType.Folder; - } - } - - /// <summary> - /// Gets the underlying base item. - /// </summary> - public BaseItem Item { get; } - - /// <summary> - /// Gets the DLNA item type. - /// </summary> - public StubType? StubType { get; } - } -} diff --git a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs deleted file mode 100644 index 7e3db4651..000000000 --- a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs +++ /dev/null @@ -1,415 +0,0 @@ -using System.Collections.Generic; -using Emby.Dlna.Common; - -namespace Emby.Dlna.ContentDirectory -{ - /// <summary> - /// Defines the <see cref="ServiceActionListBuilder" />. - /// </summary> - public static class ServiceActionListBuilder - { - /// <summary> - /// Returns a list of services that this instance provides. - /// </summary> - /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns> - public static IEnumerable<ServiceAction> GetActions() - { - return new[] - { - GetSearchCapabilitiesAction(), - GetSortCapabilitiesAction(), - GetGetSystemUpdateIDAction(), - GetBrowseAction(), - GetSearchAction(), - GetX_GetFeatureListAction(), - GetXSetBookmarkAction(), - GetBrowseByLetterAction() - }; - } - - /// <summary> - /// Returns the action details for "GetSystemUpdateID". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetGetSystemUpdateIDAction() - { - var action = new ServiceAction - { - Name = "GetSystemUpdateID" - }; - - action.ArgumentList.Add(new Argument - { - Name = "Id", - Direction = "out", - RelatedStateVariable = "SystemUpdateID" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetSearchCapabilities". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetSearchCapabilitiesAction() - { - var action = new ServiceAction - { - Name = "GetSearchCapabilities" - }; - - action.ArgumentList.Add(new Argument - { - Name = "SearchCaps", - Direction = "out", - RelatedStateVariable = "SearchCapabilities" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetSortCapabilities". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetSortCapabilitiesAction() - { - var action = new ServiceAction - { - Name = "GetSortCapabilities" - }; - - action.ArgumentList.Add(new Argument - { - Name = "SortCaps", - Direction = "out", - RelatedStateVariable = "SortCapabilities" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "X_GetFeatureList". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetX_GetFeatureListAction() - { - var action = new ServiceAction - { - Name = "X_GetFeatureList" - }; - - action.ArgumentList.Add(new Argument - { - Name = "FeatureList", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Featurelist" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "Search". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetSearchAction() - { - var action = new ServiceAction - { - Name = "Search" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ContainerID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ObjectID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "SearchCriteria", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_SearchCriteria" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Filter", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Filter" - }); - - action.ArgumentList.Add(new Argument - { - Name = "StartingIndex", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Index" - }); - - action.ArgumentList.Add(new Argument - { - Name = "RequestedCount", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "SortCriteria", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_SortCriteria" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Result", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Result" - }); - - action.ArgumentList.Add(new Argument - { - Name = "NumberReturned", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "TotalMatches", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "UpdateID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_UpdateID" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "Browse". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetBrowseAction() - { - var action = new ServiceAction - { - Name = "Browse" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ObjectID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ObjectID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "BrowseFlag", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_BrowseFlag" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Filter", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Filter" - }); - - action.ArgumentList.Add(new Argument - { - Name = "StartingIndex", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Index" - }); - - action.ArgumentList.Add(new Argument - { - Name = "RequestedCount", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "SortCriteria", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_SortCriteria" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Result", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Result" - }); - - action.ArgumentList.Add(new Argument - { - Name = "NumberReturned", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "TotalMatches", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "UpdateID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_UpdateID" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "X_BrowseByLetter". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetBrowseByLetterAction() - { - var action = new ServiceAction - { - Name = "X_BrowseByLetter" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ObjectID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ObjectID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "BrowseFlag", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_BrowseFlag" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Filter", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Filter" - }); - - action.ArgumentList.Add(new Argument - { - Name = "StartingLetter", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_BrowseLetter" - }); - - action.ArgumentList.Add(new Argument - { - Name = "RequestedCount", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "SortCriteria", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_SortCriteria" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Result", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Result" - }); - - action.ArgumentList.Add(new Argument - { - Name = "NumberReturned", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "TotalMatches", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Count" - }); - - action.ArgumentList.Add(new Argument - { - Name = "UpdateID", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_UpdateID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "StartingIndex", - Direction = "out", - RelatedStateVariable = "A_ARG_TYPE_Index" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "X_SetBookmark". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetXSetBookmarkAction() - { - var action = new ServiceAction - { - Name = "X_SetBookmark" - }; - - action.ArgumentList.Add(new Argument - { - Name = "CategoryType", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_CategoryType" - }); - - action.ArgumentList.Add(new Argument - { - Name = "RID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_RID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "ObjectID", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_ObjectID" - }); - - action.ArgumentList.Add(new Argument - { - Name = "PosSecond", - Direction = "in", - RelatedStateVariable = "A_ARG_TYPE_PosSec" - }); - - return action; - } - } -} diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs deleted file mode 100644 index 187dc1d75..000000000 --- a/Emby.Dlna/ContentDirectory/StubType.cs +++ /dev/null @@ -1,30 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna.ContentDirectory -{ - /// <summary> - /// Defines the DLNA item types. - /// </summary> - public enum StubType - { - Folder = 0, - Latest = 2, - Playlists = 3, - Albums = 4, - AlbumArtists = 5, - Artists = 6, - Songs = 7, - Genres = 8, - FavoriteSongs = 9, - FavoriteArtists = 10, - FavoriteAlbums = 11, - ContinueWatching = 12, - Movies = 13, - Collections = 14, - Favorites = 15, - NextUp = 16, - Series = 17, - FavoriteSeries = 18, - FavoriteEpisodes = 19 - } -} diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs deleted file mode 100644 index 8ee6325e9..000000000 --- a/Emby.Dlna/ControlRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.IO; -using Microsoft.AspNetCore.Http; - -namespace Emby.Dlna -{ - public class ControlRequest - { - public ControlRequest(IHeaderDictionary headers) - { - Headers = headers; - } - - public IHeaderDictionary Headers { get; } - - public Stream InputXml { get; set; } - - public string TargetServerUuId { get; set; } - - public string RequestedUrl { get; set; } - } -} diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs deleted file mode 100644 index 8b0958842..000000000 --- a/Emby.Dlna/ControlResponse.cs +++ /dev/null @@ -1,28 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace Emby.Dlna -{ - public class ControlResponse - { - public ControlResponse(string xml, bool isSuccessful) - { - Headers = new Dictionary<string, string>(); - Xml = xml; - IsSuccessful = isSuccessful; - } - - public IDictionary<string, string> Headers { get; } - - public string Xml { get; set; } - - public bool IsSuccessful { get; set; } - - /// <inheritdoc /> - public override string ToString() - { - return Xml; - } - } -} diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs deleted file mode 100644 index 9f152df13..000000000 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ /dev/null @@ -1,1266 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Linq; -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; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Playlists; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; -using Episode = MediaBrowser.Controller.Entities.TV.Episode; -using Genre = MediaBrowser.Controller.Entities.Genre; -using Movie = MediaBrowser.Controller.Entities.Movies.Movie; -using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; -using Season = MediaBrowser.Controller.Entities.TV.Season; -using Series = MediaBrowser.Controller.Entities.TV.Series; -using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute; - -namespace Emby.Dlna.Didl -{ - public class DidlBuilder - { - private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - private const string NsDc = "http://purl.org/dc/elements/1.1/"; - private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; - - private readonly DeviceProfile _profile; - private readonly IImageProcessor _imageProcessor; - private readonly string _serverAddress; - private readonly string? _accessToken; - private readonly User? _user; - private readonly IUserDataManager _userDataManager; - private readonly ILocalizationManager _localization; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly ILogger _logger; - private readonly IMediaEncoder _mediaEncoder; - private readonly ILibraryManager _libraryManager; - - public DidlBuilder( - DeviceProfile profile, - User? user, - IImageProcessor imageProcessor, - string serverAddress, - string? accessToken, - IUserDataManager userDataManager, - ILocalizationManager localization, - IMediaSourceManager mediaSourceManager, - ILogger logger, - IMediaEncoder mediaEncoder, - ILibraryManager libraryManager) - { - _profile = profile; - _user = user; - _imageProcessor = imageProcessor; - _serverAddress = serverAddress; - _accessToken = accessToken; - _userDataManager = userDataManager; - _localization = localization; - _mediaSourceManager = mediaSourceManager; - _logger = logger; - _mediaEncoder = mediaEncoder; - _libraryManager = libraryManager; - } - - public static string NormalizeDlnaMediaUrl(string url) - { - return url + "&dlnaheaders=true"; - } - - public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false, - OmitXmlDeclaration = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) - { - // If this using are changed to single lines, then write.Flush needs to be appended before the return. - using (var writer = XmlWriter.Create(builder, settings)) - { - // writer.WriteStartDocument(); - - writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - - writer.WriteAttributeString("xmlns", "dc", null, NsDc); - writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); - writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); - // didl.SetAttribute("xmlns:sec", NS_SEC); - - WriteXmlRootAttributes(_profile, writer); - - WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo); - - writer.WriteFullEndElement(); - // writer.WriteEndDocument(); - } - - return builder.ToString(); - } - } - - public static void WriteXmlRootAttributes(DeviceProfile profile, XmlWriter writer) - { - foreach (var att in profile.XmlRootAttributes) - { - var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 2) - { - writer.WriteAttributeString(parts[0], parts[1], null, att.Value); - } - else - { - writer.WriteAttributeString(att.Name, att.Value); - } - } - } - - public void WriteItemElement( - XmlWriter writer, - BaseItem item, - User? user, - BaseItem? context, - StubType? contextStubType, - string deviceId, - Filter filter, - StreamInfo? streamInfo = null) - { - var clientId = GetClientId(item, null); - - writer.WriteStartElement(string.Empty, "item", NsDidl); - - writer.WriteAttributeString("restricted", "1"); - writer.WriteAttributeString("id", clientId); - - if (context is not null) - { - writer.WriteAttributeString("parentID", GetClientId(context, contextStubType)); - } - else - { - var parent = item.DisplayParentId; - if (!parent.Equals(default)) - { - writer.WriteAttributeString("parentID", GetClientId(parent, null)); - } - } - - AddGeneralProperties(item, null, context, writer, filter); - - AddSamsungBookmarkInfo(item, user, writer, streamInfo); - - // refID? - // storeAttribute(itemNode, object, ClassProperties.REF_ID, false); - - if (item is IHasMediaSources) - { - switch (item.MediaType) - { - case MediaType.Audio: - AddAudioResource(writer, item, deviceId, filter, streamInfo); - break; - case MediaType.Video: - AddVideoResource(writer, item, deviceId, filter, streamInfo); - break; - } - } - - AddCover(item, null, writer); - writer.WriteFullEndElement(); - } - - private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null) - { - if (streamInfo is null) - { - var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user); - - streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions - { - ItemId = video.Id, - MediaSources = sources.ToArray(), - Profile = _profile, - DeviceId = deviceId, - MaxBitrate = _profile.MaxStreamingBitrate - }) ?? throw new InvalidOperationException("No optimal video stream found"); - } - - var targetWidth = streamInfo.TargetWidth; - var targetHeight = streamInfo.TargetHeight; - - var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader( - _profile, - streamInfo.Container, - streamInfo.TargetVideoCodec.FirstOrDefault(), - streamInfo.TargetAudioCodec.FirstOrDefault(), - targetWidth, - targetHeight, - streamInfo.TargetVideoBitDepth, - streamInfo.TargetVideoBitrate, - streamInfo.TargetTimestamp, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TargetVideoProfile, - streamInfo.TargetVideoRangeType, - streamInfo.TargetVideoLevel, - streamInfo.TargetFramerate ?? 0, - streamInfo.TargetPacketLength, - streamInfo.TranscodeSeekInfo, - streamInfo.IsTargetAnamorphic, - streamInfo.IsTargetInterlaced, - streamInfo.TargetRefFrames, - streamInfo.TargetVideoStreamCount, - streamInfo.TargetAudioStreamCount, - streamInfo.TargetVideoCodecTag, - streamInfo.IsTargetAVC); - - foreach (var contentFeature in contentFeatureList) - { - AddVideoResource(writer, filter, contentFeature, streamInfo); - } - - var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken); - - foreach (var subtitle in subtitleProfiles) - { - if (subtitle.DeliveryMethod != SubtitleDeliveryMethod.External) - { - continue; - } - - var subtitleAdded = AddSubtitleElement(writer, subtitle); - - if (subtitleAdded && _profile.EnableSingleSubtitleLimit) - { - break; - } - } - } - - private bool AddSubtitleElement(XmlWriter writer, SubtitleStreamInfo info) - { - var subtitleProfile = _profile.SubtitleProfiles - .FirstOrDefault(i => string.Equals(info.Format, i.Format, StringComparison.OrdinalIgnoreCase) - && i.Method == SubtitleDeliveryMethod.External); - - if (subtitleProfile is null) - { - return false; - } - - var subtitleMode = subtitleProfile.DidlMode; - - if (string.Equals(subtitleMode, "CaptionInfoEx", StringComparison.OrdinalIgnoreCase)) - { - // <sec:CaptionInfoEx sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfoEx> - // <sec:CaptionInfo sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfo> - - writer.WriteStartElement("sec", "CaptionInfoEx", null); - writer.WriteAttributeString("sec", "type", null, info.Format.ToLowerInvariant()); - - writer.WriteString(info.Url); - writer.WriteFullEndElement(); - } - else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase)) - { - writer.WriteStartElement(string.Empty, "res", NsDidl); - - writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*"); - - writer.WriteString(info.Url); - writer.WriteFullEndElement(); - } - else - { - writer.WriteStartElement(string.Empty, "res", NsDidl); - var protocolInfo = string.Format( - CultureInfo.InvariantCulture, - "http-get:*:text/{0}:*", - info.Format.ToLowerInvariant()); - writer.WriteAttributeString("protocolInfo", protocolInfo); - - writer.WriteString(info.Url); - writer.WriteFullEndElement(); - } - - return true; - } - - private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo) - { - writer.WriteStartElement(string.Empty, "res", NsDidl); - - var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken)); - - var mediaSource = streamInfo.MediaSource; - - if (mediaSource?.RunTimeTicks.HasValue == true) - { - writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); - } - - if (filter.Contains("res@size")) - { - if (streamInfo.IsDirectStream || streamInfo.EstimateContentLength) - { - var size = streamInfo.TargetSize; - - if (size.HasValue) - { - writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture)); - } - } - } - - var totalBitrate = streamInfo.TargetTotalBitrate; - var targetSampleRate = streamInfo.TargetAudioSampleRate; - var targetChannels = streamInfo.TargetAudioChannels; - - var targetWidth = streamInfo.TargetWidth; - var targetHeight = streamInfo.TargetHeight; - - if (targetChannels.HasValue) - { - writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (filter.Contains("res@resolution")) - { - if (targetWidth.HasValue && targetHeight.HasValue) - { - writer.WriteAttributeString( - "resolution", - string.Format( - CultureInfo.InvariantCulture, - "{0}x{1}", - targetWidth.Value, - targetHeight.Value)); - } - } - - if (targetSampleRate.HasValue) - { - writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (totalBitrate.HasValue) - { - writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture)); - } - - var mediaProfile = _profile.GetVideoMediaProfile( - streamInfo.Container, - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetVideoCodec.FirstOrDefault(), - streamInfo.TargetAudioBitrate, - targetWidth, - targetHeight, - streamInfo.TargetVideoBitDepth, - streamInfo.TargetVideoProfile, - streamInfo.TargetVideoRangeType, - streamInfo.TargetVideoLevel, - streamInfo.TargetFramerate ?? 0, - streamInfo.TargetPacketLength, - streamInfo.TargetTimestamp, - streamInfo.IsTargetAnamorphic, - streamInfo.IsTargetInterlaced, - streamInfo.TargetRefFrames, - streamInfo.TargetVideoStreamCount, - streamInfo.TargetAudioStreamCount, - streamInfo.TargetVideoCodecTag, - streamInfo.IsTargetAVC); - - var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal)); - - var mimeType = mediaProfile is null || string.IsNullOrEmpty(mediaProfile.MimeType) - ? MimeTypes.GetMimeType(filename) - : mediaProfile.MimeType; - - writer.WriteAttributeString( - "protocolInfo", - string.Format( - CultureInfo.InvariantCulture, - "http-get:*:{0}:{1}", - mimeType, - contentFeatures)); - - writer.WriteString(url); - - writer.WriteFullEndElement(); - } - - private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context) - { - if (itemStubType.HasValue) - { - switch (itemStubType.Value) - { - case StubType.Latest: return _localization.GetLocalizedString("Latest"); - case StubType.Playlists: return _localization.GetLocalizedString("Playlists"); - case StubType.AlbumArtists: return _localization.GetLocalizedString("HeaderAlbumArtists"); - case StubType.Albums: return _localization.GetLocalizedString("Albums"); - case StubType.Artists: return _localization.GetLocalizedString("Artists"); - case StubType.Songs: return _localization.GetLocalizedString("Songs"); - case StubType.Genres: return _localization.GetLocalizedString("Genres"); - case StubType.FavoriteAlbums: return _localization.GetLocalizedString("HeaderFavoriteAlbums"); - case StubType.FavoriteArtists: return _localization.GetLocalizedString("HeaderFavoriteArtists"); - case StubType.FavoriteSongs: return _localization.GetLocalizedString("HeaderFavoriteSongs"); - case StubType.ContinueWatching: return _localization.GetLocalizedString("HeaderContinueWatching"); - case StubType.Movies: return _localization.GetLocalizedString("Movies"); - case StubType.Collections: return _localization.GetLocalizedString("Collections"); - case StubType.Favorites: return _localization.GetLocalizedString("Favorites"); - case StubType.NextUp: return _localization.GetLocalizedString("HeaderNextUp"); - case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); - case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); - case StubType.Series: return _localization.GetLocalizedString("Shows"); - } - } - - return item is Episode episode - ? GetEpisodeDisplayName(episode, context) - : item.Name; - } - - /// <summary> - /// Gets episode display name appropriate for the given context. - /// </summary> - /// <remarks> - /// If context is a season, this will return a string containing just episode number and name. - /// Otherwise the result will include series names and season number. - /// </remarks> - /// <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) - { - string[] components; - - if (context is Season season) - { - // This is a special embedded within a season - if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value == 0 - && season.IndexNumber.HasValue && season.IndexNumber.Value != 0) - { - return string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("ValueSpecialEpisodeName"), - episode.Name); - } - - // inside a season use simple format (ex. '12 - Episode Name') - var epNumberName = GetEpisodeIndexFullName(episode); - components = new[] { epNumberName, episode.Name }; - } - else - { - // outside a season include series and season details (ex. 'TV Show - S05E11 - Episode Name') - var epNumberName = GetEpisodeNumberDisplayName(episode); - components = new[] { episode.SeriesName, epNumberName, episode.Name }; - } - - return string.Join(" - ", components.Where(NotNullOrWhiteSpace)); - } - - /// <summary> - /// Gets complete episode number. - /// </summary> - /// <param name="episode">The episode.</param> - /// <returns>For single episodes returns just the number. For double episodes - current and ending numbers.</returns> - private string GetEpisodeIndexFullName(Episode episode) - { - var name = string.Empty; - if (episode.IndexNumber.HasValue) - { - name += episode.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); - - if (episode.IndexNumberEnd.HasValue) - { - name += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); - } - } - - return name; - } - - /// <summary> - /// Gets episode number formatted as 'S##E##'. - /// </summary> - /// <param name="episode">The episode.</param> - /// <returns>Formatted episode number.</returns> - private string GetEpisodeNumberDisplayName(Episode episode) - { - var name = string.Empty; - var seasonNumber = episode.Season?.IndexNumber; - - if (seasonNumber.HasValue) - { - name = "S" + seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture); - } - - var indexName = GetEpisodeIndexFullName(episode); - - if (!string.IsNullOrWhiteSpace(indexName)) - { - name += "E" + indexName; - } - - return name; - } - - private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s); - - private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null) - { - writer.WriteStartElement(string.Empty, "res", NsDidl); - - if (streamInfo is null) - { - var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user); - - streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions - { - ItemId = audio.Id, - 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 is not null) - { - writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); - } - - if (filter.Contains("res@size")) - { - if (streamInfo.IsDirectStream || streamInfo.EstimateContentLength) - { - var size = streamInfo.TargetSize; - - if (size.HasValue) - { - writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture)); - } - } - } - - var targetAudioBitrate = streamInfo.TargetAudioBitrate; - var targetSampleRate = streamInfo.TargetAudioSampleRate; - var targetChannels = streamInfo.TargetAudioChannels; - var targetAudioBitDepth = streamInfo.TargetAudioBitDepth; - - if (targetChannels.HasValue) - { - writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (targetSampleRate.HasValue) - { - writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (targetAudioBitrate.HasValue) - { - writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); - } - - var mediaProfile = _profile.GetAudioMediaProfile( - streamInfo.Container, - streamInfo.TargetAudioCodec.FirstOrDefault(), - targetChannels, - targetAudioBitrate, - targetSampleRate, - targetAudioBitDepth); - - var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal)); - - var mimeType = mediaProfile is null || string.IsNullOrEmpty(mediaProfile.MimeType) - ? MimeTypes.GetMimeType(filename) - : mediaProfile.MimeType; - - var contentFeatures = ContentFeatureBuilder.BuildAudioHeader( - _profile, - streamInfo.Container, - streamInfo.TargetAudioCodec.FirstOrDefault(), - targetAudioBitrate, - targetSampleRate, - targetChannels, - targetAudioBitDepth, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TranscodeSeekInfo); - - writer.WriteAttributeString( - "protocolInfo", - string.Format( - CultureInfo.InvariantCulture, - "http-get:*:{0}:{1}", - mimeType, - contentFeatures)); - - writer.WriteString(url); - - writer.WriteFullEndElement(); - } - - public static bool IsIdRoot(string id) - => string.IsNullOrWhiteSpace(id) - || string.Equals(id, "0", StringComparison.OrdinalIgnoreCase) - // 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) - { - writer.WriteStartElement(string.Empty, "container", NsDidl); - - writer.WriteAttributeString("restricted", "1"); - writer.WriteAttributeString("searchable", "1"); - writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture)); - - var clientId = GetClientId(folder, stubType); - - if (string.Equals(requestedId, "0", StringComparison.Ordinal)) - { - writer.WriteAttributeString("id", "0"); - writer.WriteAttributeString("parentID", "-1"); - } - else - { - writer.WriteAttributeString("id", clientId); - - if (context is not null) - { - writer.WriteAttributeString("parentID", GetClientId(context, null)); - } - else - { - var parent = folder.DisplayParentId; - if (parent.Equals(default)) - { - writer.WriteAttributeString("parentID", "0"); - } - else - { - writer.WriteAttributeString("parentID", GetClientId(parent, null)); - } - } - } - - AddGeneralProperties(folder, stubType, context, writer, filter); - - AddCover(folder, stubType, writer); - - writer.WriteFullEndElement(); - } - - private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo) - { - if (!item.SupportsPositionTicksResume || item is Folder) - { - return; - } - - XmlAttribute? secAttribute = null; - foreach (var attribute in _profile.XmlRootAttributes) - { - if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) - { - secAttribute = attribute; - break; - } - } - - // Not a samsung device or no user data - if (secAttribute is null || user is null) - { - return; - } - - var userdata = _userDataManager.GetUserData(user, item); - var playbackPositionTicks = (streamInfo is not null && streamInfo.StartPositionTicks > 0) ? streamInfo.StartPositionTicks : userdata.PlaybackPositionTicks; - - if (playbackPositionTicks > 0) - { - var elementValue = string.Format( - CultureInfo.InvariantCulture, - "BM={0}", - Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds)); - AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value); - } - } - - /// <summary> - /// Adds fields used by both items and folders. - /// </summary> - 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 - // if (filter.Contains("dc:title")) - { - AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc); - } - - WriteObjectClass(writer, item, itemStubType); - - if (filter.Contains("dc:date")) - { - if (item.PremiereDate.HasValue) - { - AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc); - } - } - - if (filter.Contains("upnp:genre")) - { - foreach (var genre in item.Genres) - { - AddValue(writer, "upnp", "genre", genre, NsUpnp); - } - } - - foreach (var studio in item.Studios) - { - AddValue(writer, "upnp", "publisher", studio, NsUpnp); - } - - if (item is not Folder) - { - if (filter.Contains("dc:description")) - { - var desc = item.Overview; - - if (!string.IsNullOrWhiteSpace(desc)) - { - AddValue(writer, "dc", "description", desc, NsDc); - } - } - - // if (filter.Contains("upnp:longDescription")) - // { - // if (!string.IsNullOrWhiteSpace(item.Overview)) - // { - // AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp); - // } - // } - } - - if (!string.IsNullOrEmpty(item.OfficialRating)) - { - if (filter.Contains("dc:rating")) - { - AddValue(writer, "dc", "rating", item.OfficialRating, NsDc); - } - - if (filter.Contains("upnp:rating")) - { - AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp); - } - } - - AddPeople(item, writer); - } - - private void WriteObjectClass(XmlWriter writer, BaseItem item, StubType? stubType) - { - // More types here - // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs - - writer.WriteStartElement("upnp", "class", NsUpnp); - - if (item.IsDisplayedAsFolder || stubType.HasValue) - { - string? classType = null; - - if (!_profile.RequiresPlainFolders) - { - if (item is MusicAlbum) - { - classType = "object.container.album.musicAlbum"; - } - else if (item is MusicArtist) - { - classType = "object.container.person.musicArtist"; - } - else if (item is Series || item is Season || item is BoxSet || item is Video) - { - classType = "object.container.album.videoAlbum"; - } - else if (item is Playlist) - { - classType = "object.container.playlistContainer"; - } - else if (item is PhotoAlbum) - { - classType = "object.container.album.photoAlbum"; - } - } - - writer.WriteString(classType ?? "object.container.storageFolder"); - } - else if (item.MediaType == MediaType.Audio) - { - writer.WriteString("object.item.audioItem.musicTrack"); - } - else if (item.MediaType == MediaType.Photo) - { - writer.WriteString("object.item.imageItem.photo"); - } - else if (item.MediaType == MediaType.Video) - { - if (!_profile.RequiresPlainVideoItems && item is Movie) - { - writer.WriteString("object.item.videoItem.movie"); - } - else if (!_profile.RequiresPlainVideoItems && item is MusicVideo) - { - writer.WriteString("object.item.videoItem.musicVideoClip"); - } - else - { - writer.WriteString("object.item.videoItem"); - } - } - else if (item is MusicGenre) - { - writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre.musicGenre"); - } - else if (item is Genre) - { - writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre"); - } - else - { - writer.WriteString("object.item"); - } - - writer.WriteFullEndElement(); - } - - private void AddPeople(BaseItem item, XmlWriter writer) - { - if (!item.SupportsPeople) - { - return; - } - - var types = new[] - { - PersonKind.Director, - PersonKind.Writer, - PersonKind.Producer, - PersonKind.Composer, - PersonKind.Creator - }; - - // Seeing some LG models locking up due content with large lists of people - // The actual issue might just be due to processing a more metadata than it can handle - var people = _libraryManager.GetPeople( - new InternalPeopleQuery - { - ItemId = item.Id, - Limit = 6 - }); - - foreach (var actor in people) - { - 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.ToString().ToLowerInvariant(), actor.Name, NsUpnp); - } - } - - private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter) - { - AddCommonFields(item, itemStubType, context, writer, filter); - - var hasAlbumArtists = item as IHasAlbumArtist; - - if (item is IHasArtist hasArtists) - { - foreach (var artist in hasArtists.Artists) - { - AddValue(writer, "upnp", "artist", artist, NsUpnp); - AddValue(writer, "dc", "creator", artist, NsDc); - - // If it doesn't support album artists (musicvideo), then tag as both - if (hasAlbumArtists is null) - { - AddAlbumArtist(writer, artist); - } - } - } - - if (hasAlbumArtists is not null) - { - foreach (var albumArtist in hasAlbumArtists.AlbumArtists) - { - AddAlbumArtist(writer, albumArtist); - } - } - - if (!string.IsNullOrWhiteSpace(item.Album)) - { - AddValue(writer, "upnp", "album", item.Album, NsUpnp); - } - - if (item.IndexNumber.HasValue) - { - AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp); - - if (item is Episode) - { - AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp); - } - } - } - - private void AddAlbumArtist(XmlWriter writer, string name) - { - try - { - writer.WriteStartElement("upnp", "artist", NsUpnp); - writer.WriteAttributeString("role", "AlbumArtist"); - - writer.WriteString(name); - - writer.WriteFullEndElement(); - } - catch (XmlException ex) - { - _logger.LogError(ex, "Error adding xml value: {Value}", name); - } - } - - private void AddValue(XmlWriter writer, string prefix, string name, string value, string namespaceUri) - { - try - { - writer.WriteElementString(prefix, name, namespaceUri, value); - } - catch (XmlException ex) - { - _logger.LogError(ex, "Error adding xml value: {Value}", value); - } - } - - private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer) - { - ImageDownloadInfo? imageInfo = GetImageInfo(item); - - if (imageInfo is null) - { - return; - } - - // TODO: Remove these default values - var albumArtUrlInfo = GetImageUrl( - imageInfo, - _profile.MaxAlbumArtWidth ?? 10000, - _profile.MaxAlbumArtHeight ?? 10000, - "jpg"); - - writer.WriteStartElement("upnp", "albumArtURI", NsUpnp); - if (!string.IsNullOrEmpty(_profile.AlbumArtPn)) - { - writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); - } - - writer.WriteString(albumArtUrlInfo.Url); - writer.WriteFullEndElement(); - - // TODO: Remove these default values - var iconUrlInfo = GetImageUrl( - imageInfo, - _profile.MaxIconWidth ?? 48, - _profile.MaxIconHeight ?? 48, - "jpg"); - writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.Url); - - if (!_profile.EnableAlbumArtInDidl) - { - if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video) - { - if (!stubType.HasValue) - { - return; - } - } - } - - if (!_profile.EnableSingleAlbumArtLimit || item.MediaType == MediaType.Photo) - { - AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); - AddImageResElement(item, writer, 1024, 768, "jpg", "JPEG_MED"); - AddImageResElement(item, writer, 640, 480, "jpg", "JPEG_SM"); - AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG"); - AddImageResElement(item, writer, 160, 160, "png", "PNG_TN"); - } - - AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN"); - } - - private void AddImageResElement( - BaseItem item, - XmlWriter writer, - int maxWidth, - int maxHeight, - string format, - string org_Pn) - { - var imageInfo = GetImageInfo(item); - - if (imageInfo is null) - { - return; - } - - var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format); - - writer.WriteStartElement(string.Empty, "res", NsDidl); - - // Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail - // rather than using a larger one when available - var width = albumartUrlInfo.Width ?? maxWidth; - var height = albumartUrlInfo.Height ?? maxHeight; - - var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn); - - writer.WriteAttributeString( - "protocolInfo", - string.Format( - CultureInfo.InvariantCulture, - "http-get:*:{0}:{1}", - MimeTypes.GetMimeType("file." + format), - contentFeatures)); - - writer.WriteAttributeString( - "resolution", - string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height)); - - writer.WriteString(albumartUrlInfo.Url); - - writer.WriteFullEndElement(); - } - - private ImageDownloadInfo? GetImageInfo(BaseItem item) - { - if (item.HasImage(ImageType.Primary)) - { - return GetImageInfo(item, ImageType.Primary); - } - - if (item.HasImage(ImageType.Thumb)) - { - return GetImageInfo(item, ImageType.Thumb); - } - - if (item.HasImage(ImageType.Backdrop)) - { - if (item is Channel) - { - return GetImageInfo(item, ImageType.Backdrop); - } - } - - // For audio tracks without art use album art if available. - if (item is Audio audioItem) - { - var album = audioItem.AlbumEntity; - return album is not null && album.HasImage(ImageType.Primary) - ? GetImageInfo(album, ImageType.Primary) - : null; - } - - // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder. - if (item is MusicAlbum || item is Playlist) - { - return null; - } - - // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item. - var parentWithImage = GetFirstParentWithImageBelowUserRoot(item); - if (parentWithImage is not null) - { - return GetImageInfo(parentWithImage, ImageType.Primary); - } - - return null; - } - - private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item) - { - if (item is null) - { - return null; - } - - if (item.HasImage(ImageType.Primary)) - { - return item; - } - - var parent = item.GetParent(); - if (parent is UserRootFolder) - { - return null; - } - - // terminate in case we went past user root folder (unlikely?) - if (parent is Folder folder && folder.IsRoot) - { - return null; - } - - return GetFirstParentWithImageBelowUserRoot(parent); - } - - private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) - { - var imageInfo = item.GetImageInfo(type, 0); - string? tag = null; - - try - { - tag = _imageProcessor.GetImageCacheTag(item, type); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting image cache tag"); - } - - int? width = imageInfo.Width; - int? height = imageInfo.Height; - - if (width == 0 || height == 0) - { - width = null; - height = null; - } - else if (width == -1 || height == -1) - { - width = null; - height = null; - } - - var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty) - .TrimStart('.') - .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); - - return new ImageDownloadInfo - { - ItemId = item.Id, - Type = type, - ImageTag = tag, - Width = width, - Height = height, - Format = inputFormat, - ItemImageInfo = imageInfo - }; - } - - public static string GetClientId(BaseItem item, StubType? stubType) - { - return GetClientId(item.Id, stubType); - } - - public static string GetClientId(Guid idValue, StubType? stubType) - { - var id = idValue.ToString("N", CultureInfo.InvariantCulture); - - if (stubType.HasValue) - { - id = stubType.Value.ToString().ToLowerInvariant() + "_" + id; - } - - return id; - } - - private (string Url, int? Width, int? Height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format) - { - var url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0", - _serverAddress, - info.ItemId.ToString("N", CultureInfo.InvariantCulture), - info.Type, - info.ImageTag, - format, - maxWidth.ToString(CultureInfo.InvariantCulture), - maxHeight.ToString(CultureInfo.InvariantCulture)); - - var width = info.Width; - var height = info.Height; - - info.IsDirectStream = false; - - if (width.HasValue && height.HasValue) - { - var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); - - width = newSize.Width; - height = newSize.Height; - - var normalizedFormat = format - .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); - - if (string.Equals(info.Format, normalizedFormat, StringComparison.OrdinalIgnoreCase)) - { - info.IsDirectStream = maxWidth >= width.Value && maxHeight >= height.Value; - } - } - - // just lie - info.IsDirectStream = true; - - return (url, width, height); - } - - private class ImageDownloadInfo - { - internal Guid ItemId { get; set; } - - internal string? ImageTag { get; set; } - - internal ImageType Type { get; set; } - - internal int? Width { get; set; } - - internal int? Height { get; set; } - - internal bool IsDirectStream { get; set; } - - internal required string Format { get; set; } - - internal required ItemImageInfo ItemImageInfo { get; set; } - } - } -} diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs deleted file mode 100644 index 6db6f3ae3..000000000 --- a/Emby.Dlna/Didl/Filter.cs +++ /dev/null @@ -1,28 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.Didl -{ - public class Filter - { - private readonly string[] _fields; - private readonly bool _all; - - public Filter() - : this("*") - { - } - - public Filter(string filter) - { - _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase); - _fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries); - } - - public bool Contains(string field) - { - return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase)); - } - } -} diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs deleted file mode 100644 index b66f53ece..000000000 --- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs +++ /dev/null @@ -1,58 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable CA1305 - -using System; -using System.IO; -using System.Text; - -namespace Emby.Dlna.Didl -{ - public class StringWriterWithEncoding : StringWriter - { - private readonly Encoding? _encoding; - - public StringWriterWithEncoding() - { - } - - public StringWriterWithEncoding(IFormatProvider formatProvider) - : base(formatProvider) - { - } - - public StringWriterWithEncoding(StringBuilder sb) - : base(sb) - { - } - - public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider) - : base(sb, formatProvider) - { - } - - public StringWriterWithEncoding(Encoding encoding) - { - _encoding = encoding; - } - - public StringWriterWithEncoding(IFormatProvider formatProvider, Encoding encoding) - : base(formatProvider) - { - _encoding = encoding; - } - - public StringWriterWithEncoding(StringBuilder sb, Encoding encoding) - : base(sb) - { - _encoding = encoding; - } - - public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider, Encoding encoding) - : base(sb, formatProvider) - { - _encoding = encoding; - } - - public override Encoding Encoding => _encoding ?? base.Encoding; - } -} diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs deleted file mode 100644 index 6cc6b73a0..000000000 --- a/Emby.Dlna/DlnaConfigurationFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using Emby.Dlna.Configuration; -using MediaBrowser.Common.Configuration; - -namespace Emby.Dlna -{ - public class DlnaConfigurationFactory : IConfigurationFactory - { - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new[] - { - new ConfigurationStore - { - Key = "dlna", - ConfigurationType = typeof(DlnaOptions) - } - }; - } - } -} diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs deleted file mode 100644 index d67cb67b5..000000000 --- a/Emby.Dlna/DlnaManager.cs +++ /dev/null @@ -1,491 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Emby.Dlna.Profiles; -using Emby.Dlna.Server; -using Jellyfin.Extensions.Json; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; - -namespace Emby.Dlna -{ - public class DlnaManager : IDlnaManager - { - private readonly IApplicationPaths _appPaths; - private readonly IXmlSerializer _xmlSerializer; - private readonly IFileSystem _fileSystem; - private readonly ILogger<DlnaManager> _logger; - private readonly IServerApplicationHost _appHost; - private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - - private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); - - public DlnaManager( - IXmlSerializer xmlSerializer, - IFileSystem fileSystem, - IApplicationPaths appPaths, - ILoggerFactory loggerFactory, - IServerApplicationHost appHost) - { - _xmlSerializer = xmlSerializer; - _fileSystem = fileSystem; - _appPaths = appPaths; - _logger = loggerFactory.CreateLogger<DlnaManager>(); - _appHost = appHost; - } - - private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user"); - - private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system"); - - public async Task InitProfilesAsync() - { - try - { - await ExtractSystemProfilesAsync().ConfigureAwait(false); - Directory.CreateDirectory(UserProfilesPath); - LoadProfiles(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error extracting DLNA profiles."); - } - } - - private void LoadProfiles() - { - var list = GetProfiles(UserProfilesPath, DeviceProfileType.User) - .OrderBy(i => i.Name) - .ToList(); - - list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System) - .OrderBy(i => i.Name)); - } - - public IEnumerable<DeviceProfile> GetProfiles() - { - lock (_profiles) - { - return _profiles.Values - .OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1) - .ThenBy(i => i.Item1.Info.Name) - .Select(i => i.Item2) - .ToList(); - } - } - - /// <inheritdoc /> - public DeviceProfile GetDefaultProfile() - { - return new DefaultProfile(); - } - - /// <inheritdoc /> - public DeviceProfile? GetProfile(DeviceIdentification deviceInfo) - { - ArgumentNullException.ThrowIfNull(deviceInfo); - - var profile = GetProfiles() - .FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification)); - - if (profile is null) - { - _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo); - } - else - { - _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name); - } - - return profile; - } - - /// <summary> - /// Attempts to match a device with a profile. - /// Rules: - /// - If the profile field has no value, the field matches regardless of its contents. - /// - the profile field can be an exact match, or a reg exp. - /// </summary> - /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param> - /// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param> - /// <returns><b>True</b> if they match.</returns> - public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) - { - return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName) - && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer) - && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl) - && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription) - && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName) - && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber) - && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl) - && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber); - } - - private bool IsRegexOrSubstringMatch(string input, string pattern) - { - if (string.IsNullOrEmpty(pattern)) - { - // In profile identification: An empty pattern matches anything. - return true; - } - - if (string.IsNullOrEmpty(input)) - { - // The profile contains a value, and the device doesn't. - return false; - } - - try - { - return input.Equals(pattern, StringComparison.OrdinalIgnoreCase) - || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - } - catch (ArgumentException ex) - { - _logger.LogError(ex, "Error evaluating regex pattern {Pattern}", pattern); - return false; - } - } - - /// <inheritdoc /> - public DeviceProfile? GetProfile(IHeaderDictionary headers) - { - ArgumentNullException.ThrowIfNull(headers); - - var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification)); - if (profile is null) - { - _logger.LogDebug("No matching device profile found. {@Headers}", headers); - } - else - { - _logger.LogDebug("Found matching device profile: {0}", profile.Name); - } - - return profile; - } - - private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo) - { - return profileInfo.Headers.Any(i => IsMatch(headers, i)); - } - - private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header) - { - // Handle invalid user setup - if (string.IsNullOrEmpty(header.Name)) - { - return false; - } - - if (headers.TryGetValue(header.Name, out StringValues value)) - { - if (StringValues.IsNullOrEmpty(value)) - { - return false; - } - - switch (header.Match) - { - case HeaderMatchType.Equals: - return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase); - case HeaderMatchType.Substring: - var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; - // _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch); - return isMatch; - case HeaderMatchType.Regex: - // Can't be null, we checked above the switch statement - return Regex.IsMatch(value!, header.Value, RegexOptions.IgnoreCase); - default: - throw new ArgumentException("Unrecognized HeaderMatchType"); - } - } - - return false; - } - - private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type) - { - try - { - return _fileSystem.GetFilePaths(path) - .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 - } - catch (IOException) - { - return Array.Empty<DeviceProfile>(); - } - } - - private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type) - { - lock (_profiles) - { - if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple)) - { - return profileTuple.Item2; - } - - try - { - var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path); - var profile = ReserializeProfile(tempProfile); - - profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); - - _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); - - return profile; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error parsing profile file: {Path}", path); - - return null; - } - } - } - - /// <inheritdoc /> - public DeviceProfile? GetProfile(string id) - { - ArgumentException.ThrowIfNullOrEmpty(id); - - var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); - - if (info is null) - { - return null; - } - - return ParseProfileFile(info.Path, info.Info.Type); - } - - private IEnumerable<InternalProfileInfo> GetProfileInfosInternal() - { - lock (_profiles) - { - return _profiles.Values - .Select(i => i.Item1) - .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1) - .ThenBy(i => i.Info.Name); - } - } - - /// <inheritdoc /> - public IEnumerable<DeviceProfileInfo> GetProfileInfos() - { - return GetProfileInfosInternal().Select(i => i.Info); - } - - private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type) - { - return new InternalProfileInfo( - new DeviceProfileInfo - { - Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture), - Name = _fileSystem.GetFileNameWithoutExtension(file), - Type = type - }, - file.FullName); - } - - private async Task ExtractSystemProfilesAsync() - { - var namespaceName = GetType().Namespace + ".Profiles.Xml."; - - var systemProfilesPath = SystemProfilesPath; - - foreach (var name in _assembly.GetManifestResourceNames()) - { - if (!name.StartsWith(namespaceName, StringComparison.Ordinal)) - { - continue; - } - - var path = Path.Join( - systemProfilesPath, - Path.GetFileName(name.AsSpan())[namespaceName.Length..]); - - if (File.Exists(path)) - { - continue; - } - - // The stream should exist as we just got its name from GetManifestResourceNames - using (var stream = _assembly.GetManifestResourceStream(name)!) - { - Directory.CreateDirectory(systemProfilesPath); - - var fileOptions = AsyncFile.WriteOptions; - fileOptions.Mode = FileMode.CreateNew; - fileOptions.PreallocationSize = stream.Length; - var fileStream = new FileStream(path, fileOptions); - await using (fileStream.ConfigureAwait(false)) - { - await stream.CopyToAsync(fileStream).ConfigureAwait(false); - } - } - } - } - - /// <inheritdoc /> - public void DeleteProfile(string id) - { - var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase)); - - if (info.Info.Type == DeviceProfileType.System) - { - throw new ArgumentException("System profiles cannot be deleted."); - } - - _fileSystem.DeleteFile(info.Path); - - lock (_profiles) - { - _profiles.Remove(info.Path); - } - } - - /// <inheritdoc /> - public void CreateProfile(DeviceProfile profile) - { - profile = ReserializeProfile(profile); - - ArgumentException.ThrowIfNullOrEmpty(profile.Name); - - var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; - var path = Path.Combine(UserProfilesPath, newFilename); - - SaveProfile(profile, path, DeviceProfileType.User); - } - - /// <inheritdoc /> - public void UpdateProfile(string profileId, DeviceProfile profile) - { - profile = ReserializeProfile(profile); - - ArgumentException.ThrowIfNullOrEmpty(profile.Id); - - ArgumentException.ThrowIfNullOrEmpty(profile.Name); - - var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase)); - if (current.Info.Type == DeviceProfileType.System) - { - throw new ArgumentException("System profiles can't be edited"); - } - - var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; - var path = Path.Join(UserProfilesPath, newFilename); - - if (!string.Equals(path, current.Path, StringComparison.Ordinal)) - { - lock (_profiles) - { - _profiles.Remove(current.Path); - } - } - - SaveProfile(profile, path, DeviceProfileType.User); - } - - private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type) - { - lock (_profiles) - { - _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); - } - - SerializeToXml(profile, path); - } - - internal void SerializeToXml(DeviceProfile profile, string path) - { - _xmlSerializer.SerializeToFile(profile, path); - } - - /// <summary> - /// Recreates the object using serialization, to ensure it's not a subclass. - /// If it's a subclass it may not serialize properly to xml (different root element tag name). - /// </summary> - /// <param name="profile">The device profile.</param> - /// <returns>The re-serialized device profile.</returns> - private DeviceProfile ReserializeProfile(DeviceProfile profile) - { - if (profile.GetType() == typeof(DeviceProfile)) - { - return profile; - } - - var json = JsonSerializer.Serialize(profile, _jsonOptions); - - // Output can't be null if the input isn't null - return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!; - } - - /// <inheritdoc /> - public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) - { - var profile = GetProfile(headers) ?? GetDefaultProfile(); - - var serverId = _appHost.SystemId; - - return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml(); - } - - /// <inheritdoc /> - public ImageStream? GetIcon(string filename) - { - var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase) - ? ImageFormat.Png - : ImageFormat.Jpg; - - var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant(); - var stream = _assembly.GetManifestResourceStream(resource); - if (stream is null) - { - return null; - } - - return new ImageStream(stream) - { - Format = format - }; - } - - private class InternalProfileInfo - { - internal InternalProfileInfo(DeviceProfileInfo info, string path) - { - Info = info; - Path = path; - } - - internal DeviceProfileInfo Info { get; } - - internal string Path { get; } - } - } -} diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj deleted file mode 100644 index 7336482e5..000000000 --- a/Emby.Dlna/Emby.Dlna.csproj +++ /dev/null @@ -1,90 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> - <PropertyGroup> - <ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid> - </PropertyGroup> - - <ItemGroup> - <Compile Include="..\SharedVersion.cs" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> - <ProjectReference Include="..\RSSDP\RSSDP.csproj" /> - </ItemGroup> - - <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> - <GenerateAssemblyInfo>false</GenerateAssemblyInfo> - <GenerateDocumentationFile>true</GenerateDocumentationFile> - </PropertyGroup> - - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors> - </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> - <EmbeddedResource Include="Images\logo120.jpg" /> - <EmbeddedResource Include="Images\logo120.png" /> - <EmbeddedResource Include="Images\logo240.jpg" /> - <EmbeddedResource Include="Images\logo240.png" /> - <EmbeddedResource Include="Images\logo48.jpg" /> - <EmbeddedResource Include="Images\logo48.png" /> - <EmbeddedResource Include="Images\people48.jpg" /> - <EmbeddedResource Include="Images\people48.png" /> - <EmbeddedResource Include="Images\people480.jpg" /> - <EmbeddedResource Include="Images\people480.png" /> - </ItemGroup> - - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Default.xml" /> - <EmbeddedResource Include="Profiles\Xml\Denon AVR.xml" /> - <EmbeddedResource Include="Profiles\Xml\DirecTV HD-DVR.xml" /> - <EmbeddedResource Include="Profiles\Xml\Dish Hopper-Joey.xml" /> - <EmbeddedResource Include="Profiles\Xml\foobar2000.xml" /> - <EmbeddedResource Include="Profiles\Xml\LG Smart TV.xml" /> - <EmbeddedResource Include="Profiles\Xml\Linksys DMA2100.xml" /> - <EmbeddedResource Include="Profiles\Xml\Marantz.xml" /> - <EmbeddedResource Include="Profiles\Xml\MediaMonkey.xml" /> - <EmbeddedResource Include="Profiles\Xml\Panasonic Viera.xml" /> - <EmbeddedResource Include="Profiles\Xml\Popcorn Hour.xml" /> - <EmbeddedResource Include="Profiles\Xml\Samsung Smart TV.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2013.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2014.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2015.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2016.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282010%29.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282011%29.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282012%29.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282013%29.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282014%29.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 3.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 4.xml" /> - <EmbeddedResource Include="Profiles\Xml\WDTV Live.xml" /> - <EmbeddedResource Include="Profiles\Xml\Xbox One.xml" /> - </ItemGroup> - - <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Http" /> - </ItemGroup> - -</Project> diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs deleted file mode 100644 index 635d2c47a..000000000 --- a/Emby.Dlna/EventSubscriptionResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace Emby.Dlna -{ - public class EventSubscriptionResponse - { - public EventSubscriptionResponse(string content, string contentType) - { - Content = content; - ContentType = contentType; - Headers = new Dictionary<string, string>(); - } - - public string Content { get; set; } - - public string ContentType { get; set; } - - public Dictionary<string, string> Headers { get; } - } -} diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs deleted file mode 100644 index ecbbdf9df..000000000 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ /dev/null @@ -1,183 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Net.Mime; -using System.Text; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.Eventing -{ - public class DlnaEventManager : IDlnaEventManager - { - private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions = - new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase); - - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - - public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl) - { - var subscription = GetSubscription(subscriptionId, false); - if (subscription is not null) - { - subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300; - int timeoutSeconds = subscription.TimeoutSeconds; - subscription.SubscriptionTime = DateTime.UtcNow; - - _logger.LogDebug( - "Renewing event subscription for {0} with timeout of {1} to {2}", - subscription.NotificationType, - timeoutSeconds, - subscription.CallbackUrl); - - return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds); - } - - return new EventSubscriptionResponse(string.Empty, "text/plain"); - } - - public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) - { - var timeout = ParseTimeout(requestedTimeoutString) ?? 300; - var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - - _logger.LogDebug( - "Creating event subscription for {0} with timeout of {1} to {2}", - notificationType, - timeout, - callbackUrl); - - _subscriptions.TryAdd(id, new EventSubscription - { - Id = id, - CallbackUrl = callbackUrl, - SubscriptionTime = DateTime.UtcNow, - TimeoutSeconds = timeout, - NotificationType = notificationType - }); - - return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout); - } - - private int? ParseTimeout(string header) - { - if (!string.IsNullOrEmpty(header)) - { - // Starts with SECOND- - if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return val; - } - } - - return null; - } - - public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) - { - _logger.LogDebug("Cancelling event subscription {0}", subscriptionId); - - _subscriptions.TryRemove(subscriptionId, out _); - - return new EventSubscriptionResponse(string.Empty, "text/plain"); - } - - private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds) - { - var response = new EventSubscriptionResponse(string.Empty, "text/plain"); - - response.Headers["SID"] = subscriptionId; - response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString; - - return response; - } - - public EventSubscription GetSubscription(string id) - { - return GetSubscription(id, false); - } - - private EventSubscription GetSubscription(string id, bool throwOnMissing) - { - if (!_subscriptions.TryGetValue(id, out EventSubscription e) && throwOnMissing) - { - throw new ResourceNotFoundException("Event with Id " + id + " not found."); - } - - return e; - } - - public Task TriggerEvent(string notificationType, IDictionary<string, string> stateVariables) - { - var subs = _subscriptions.Values - .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase)); - - var tasks = subs.Select(i => TriggerEvent(i, stateVariables)); - - return Task.WhenAll(tasks); - } - - private async Task TriggerEvent(EventSubscription subscription, IDictionary<string, string> stateVariables) - { - var builder = new StringBuilder(); - - builder.Append("<?xml version=\"1.0\"?>"); - builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); - foreach (var key in stateVariables.Keys) - { - builder.Append("<e:property>") - .Append('<') - .Append(key) - .Append('>') - .Append(stateVariables[key]) - .Append("</") - .Append(key) - .Append('>') - .Append("</e:property>"); - } - - builder.Append("</e:propertyset>"); - - using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl); - options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml); - options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); - options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); - options.Headers.TryAddWithoutValidation("SID", subscription.Id); - options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture)); - - try - { - using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp) - .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch - { - // Already logged at lower levels - } - finally - { - subscription.IncrementTriggerCount(); - } - } - } -} diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs deleted file mode 100644 index 4fd7f8169..000000000 --- a/Emby.Dlna/Eventing/EventSubscription.cs +++ /dev/null @@ -1,35 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.Eventing -{ - public class EventSubscription - { - public string Id { get; set; } - - public string CallbackUrl { get; set; } - - public string NotificationType { get; set; } - - public DateTime SubscriptionTime { get; set; } - - public int TimeoutSeconds { get; set; } - - public long TriggerCount { get; set; } - - public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow; - - public void IncrementTriggerCount() - { - if (TriggerCount == long.MaxValue) - { - TriggerCount = 0; - } - - TriggerCount++; - } - } -} diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs deleted file mode 100644 index 82c80070a..000000000 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Globalization; -using System.Net; -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; -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; - -/// <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 <see cref="IServerApplicationHost"/>.</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>(); - 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 - }); - - services.AddHostedService<DlnaHost>(); - } -} diff --git a/Emby.Dlna/IConnectionManager.cs b/Emby.Dlna/IConnectionManager.cs deleted file mode 100644 index 9f643a9e6..000000000 --- a/Emby.Dlna/IConnectionManager.cs +++ /dev/null @@ -1,8 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna -{ - public interface IConnectionManager : IDlnaEventManager, IUpnpService - { - } -} diff --git a/Emby.Dlna/IContentDirectory.cs b/Emby.Dlna/IContentDirectory.cs deleted file mode 100644 index 10f4d6386..000000000 --- a/Emby.Dlna/IContentDirectory.cs +++ /dev/null @@ -1,8 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna -{ - public interface IContentDirectory : IDlnaEventManager, IUpnpService - { - } -} diff --git a/Emby.Dlna/IDlnaEventManager.cs b/Emby.Dlna/IDlnaEventManager.cs deleted file mode 100644 index bb1eeb963..000000000 --- a/Emby.Dlna/IDlnaEventManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -namespace Emby.Dlna -{ - public interface IDlnaEventManager - { - /// <summary> - /// Cancels the event subscription. - /// </summary> - /// <param name="subscriptionId">The subscription identifier.</param> - /// <returns>The response.</returns> - EventSubscriptionResponse CancelEventSubscription(string subscriptionId); - - /// <summary> - /// Renews the event subscription. - /// </summary> - /// <param name="subscriptionId">The subscription identifier.</param> - /// <param name="notificationType">The notification type.</param> - /// <param name="requestedTimeoutString">The requested timeout as a string.</param> - /// <param name="callbackUrl">The callback url.</param> - /// <returns>The response.</returns> - EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl); - - /// <summary> - /// Creates the event subscription. - /// </summary> - /// <param name="notificationType">The notification type.</param> - /// <param name="requestedTimeoutString">The requested timeout as a string.</param> - /// <param name="callbackUrl">The callback url.</param> - /// <returns>The response.</returns> - EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl); - } -} diff --git a/Emby.Dlna/IMediaReceiverRegistrar.cs b/Emby.Dlna/IMediaReceiverRegistrar.cs deleted file mode 100644 index 43e934b53..000000000 --- a/Emby.Dlna/IMediaReceiverRegistrar.cs +++ /dev/null @@ -1,8 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna -{ - public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService - { - } -} diff --git a/Emby.Dlna/IUpnpService.cs b/Emby.Dlna/IUpnpService.cs deleted file mode 100644 index 9e7859567..000000000 --- a/Emby.Dlna/IUpnpService.cs +++ /dev/null @@ -1,22 +0,0 @@ -#pragma warning disable CS1591 - -using System.Threading.Tasks; - -namespace Emby.Dlna -{ - public interface IUpnpService - { - /// <summary> - /// Gets the content directory XML. - /// </summary> - /// <returns>System.String.</returns> - string GetServiceXml(); - - /// <summary> - /// Processes the control request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>ControlResponse.</returns> - Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request); - } -} diff --git a/Emby.Dlna/Images/logo120.jpg b/Emby.Dlna/Images/logo120.jpg Binary files differdeleted file mode 100644 index c70f4db0d..000000000 --- a/Emby.Dlna/Images/logo120.jpg +++ /dev/null diff --git a/Emby.Dlna/Images/logo120.png b/Emby.Dlna/Images/logo120.png Binary files differdeleted file mode 100644 index 14f6c8d5f..000000000 --- a/Emby.Dlna/Images/logo120.png +++ /dev/null diff --git a/Emby.Dlna/Images/logo240.jpg b/Emby.Dlna/Images/logo240.jpg Binary files differdeleted file mode 100644 index 78a27f1b5..000000000 --- a/Emby.Dlna/Images/logo240.jpg +++ /dev/null diff --git a/Emby.Dlna/Images/logo240.png b/Emby.Dlna/Images/logo240.png Binary files differdeleted file mode 100644 index ff50314d4..000000000 --- a/Emby.Dlna/Images/logo240.png +++ /dev/null diff --git a/Emby.Dlna/Images/logo48.jpg b/Emby.Dlna/Images/logo48.jpg Binary files differdeleted file mode 100644 index 269bcf589..000000000 --- a/Emby.Dlna/Images/logo48.jpg +++ /dev/null diff --git a/Emby.Dlna/Images/logo48.png b/Emby.Dlna/Images/logo48.png Binary files differdeleted file mode 100644 index d6b5fd1df..000000000 --- a/Emby.Dlna/Images/logo48.png +++ /dev/null diff --git a/Emby.Dlna/Images/people48.jpg b/Emby.Dlna/Images/people48.jpg Binary files differdeleted file mode 100644 index 3ed287062..000000000 --- a/Emby.Dlna/Images/people48.jpg +++ /dev/null diff --git a/Emby.Dlna/Images/people48.png b/Emby.Dlna/Images/people48.png Binary files differdeleted file mode 100644 index dae5f6057..000000000 --- a/Emby.Dlna/Images/people48.png +++ /dev/null diff --git a/Emby.Dlna/Images/people480.jpg b/Emby.Dlna/Images/people480.jpg Binary files differdeleted file mode 100644 index 01a316206..000000000 --- a/Emby.Dlna/Images/people480.jpg +++ /dev/null diff --git a/Emby.Dlna/Images/people480.png b/Emby.Dlna/Images/people480.png Binary files differdeleted file mode 100644 index 800a3d804..000000000 --- a/Emby.Dlna/Images/people480.png +++ /dev/null diff --git a/Emby.Dlna/Main/DlnaHost.cs b/Emby.Dlna/Main/DlnaHost.cs deleted file mode 100644 index 58db7c26f..000000000 --- a/Emby.Dlna/Main/DlnaHost.cs +++ /dev/null @@ -1,387 +0,0 @@ -#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 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 = NetworkUtils.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.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs deleted file mode 100644 index d8fb12742..000000000 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml; -using Emby.Dlna.Service; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.MediaReceiverRegistrar -{ - /// <summary> - /// Defines the <see cref="ControlHandler" />. - /// </summary> - public class ControlHandler : BaseControlHandler - { - /// <summary> - /// Initializes a new instance of the <see cref="ControlHandler"/> class. - /// </summary> - /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> - /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param> - public ControlHandler(IServerConfigurationManager config, ILogger logger) - : base(config, logger) - { - } - - /// <inheritdoc /> - protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) - { - if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) - { - HandleIsAuthorized(xmlWriter); - return; - } - - if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase)) - { - HandleIsValidated(xmlWriter); - return; - } - - throw new ResourceNotFoundException("Unexpected control request name: " + methodName); - } - - /// <summary> - /// Records that the handle is authorized in the xml stream. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private static void HandleIsAuthorized(XmlWriter xmlWriter) - => xmlWriter.WriteElementString("Result", "1"); - - /// <summary> - /// Records that the handle is validated in the xml stream. - /// </summary> - /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - private static void HandleIsValidated(XmlWriter xmlWriter) - => xmlWriter.WriteElementString("Result", "1"); - } -} diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs deleted file mode 100644 index a5aae515c..000000000 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Net.Http; -using System.Threading.Tasks; -using Emby.Dlna.Service; -using MediaBrowser.Controller.Configuration; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.MediaReceiverRegistrar -{ - /// <summary> - /// Defines the <see cref="MediaReceiverRegistrarService" />. - /// </summary> - public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar - { - private readonly IServerConfigurationManager _config; - - /// <summary> - /// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class. - /// </summary> - /// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param> - /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param> - /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param> - public MediaReceiverRegistrarService( - ILogger<MediaReceiverRegistrarService> logger, - IHttpClientFactory httpClientFactory, - IServerConfigurationManager config) - : base(logger, httpClientFactory) - { - _config = config; - } - - /// <inheritdoc /> - public string GetServiceXml() - { - return MediaReceiverRegistrarXmlBuilder.GetXml(); - } - - /// <inheritdoc /> - public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) - { - return new ControlHandler( - _config, - Logger) - .ProcessControlRequestAsync(request); - } - } -} diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs deleted file mode 100644 index f3789a791..000000000 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections.Generic; -using Emby.Dlna.Common; -using Emby.Dlna.Service; - -namespace Emby.Dlna.MediaReceiverRegistrar -{ - /// <summary> - /// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />. - /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482. - /// </summary> - public static class MediaReceiverRegistrarXmlBuilder - { - /// <summary> - /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar. - /// </summary> - /// <returns>An XML representation of this service.</returns> - public static string GetXml() - { - return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); - } - - /// <summary> - /// The a list of all the state variables for this invocation. - /// </summary> - /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> - private static IEnumerable<StateVariable> GetStateVariables() - { - var list = new List<StateVariable> - { - new StateVariable - { - Name = "AuthorizationGrantedUpdateID", - DataType = "ui4", - SendsEvents = true - }, - - new StateVariable - { - Name = "A_ARG_TYPE_DeviceID", - DataType = "string", - SendsEvents = false - }, - - new StateVariable - { - Name = "AuthorizationDeniedUpdateID", - DataType = "ui4", - SendsEvents = true - }, - - new StateVariable - { - Name = "ValidationSucceededUpdateID", - DataType = "ui4", - SendsEvents = true - }, - - new StateVariable - { - Name = "A_ARG_TYPE_RegistrationRespMsg", - DataType = "bin.base64", - SendsEvents = false - }, - - new StateVariable - { - Name = "A_ARG_TYPE_RegistrationReqMsg", - DataType = "bin.base64", - SendsEvents = false - }, - - new StateVariable - { - Name = "ValidationRevokedUpdateID", - DataType = "ui4", - SendsEvents = true - }, - - new StateVariable - { - Name = "A_ARG_TYPE_Result", - DataType = "int", - SendsEvents = false - } - }; - - return list; - } - } -} diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs deleted file mode 100644 index 56788ae22..000000000 --- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System.Collections.Generic; -using Emby.Dlna.Common; - -namespace Emby.Dlna.MediaReceiverRegistrar -{ - /// <summary> - /// Defines the <see cref="ServiceActionListBuilder" />. - /// </summary> - public static class ServiceActionListBuilder - { - /// <summary> - /// Returns a list of services that this instance provides. - /// </summary> - /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns> - public static IEnumerable<ServiceAction> GetActions() - { - return new[] - { - GetIsValidated(), - GetIsAuthorized(), - GetRegisterDevice(), - GetGetAuthorizationDeniedUpdateID(), - GetGetAuthorizationGrantedUpdateID(), - GetGetValidationRevokedUpdateID(), - GetGetValidationSucceededUpdateID() - }; - } - - /// <summary> - /// Returns the action details for "IsValidated". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetIsValidated() - { - var action = new ServiceAction - { - Name = "IsValidated" - }; - - action.ArgumentList.Add(new Argument - { - Name = "DeviceID", - Direction = "in" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Result", - Direction = "out" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "IsAuthorized". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetIsAuthorized() - { - var action = new ServiceAction - { - Name = "IsAuthorized" - }; - - action.ArgumentList.Add(new Argument - { - Name = "DeviceID", - Direction = "in" - }); - - action.ArgumentList.Add(new Argument - { - Name = "Result", - Direction = "out" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "RegisterDevice". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetRegisterDevice() - { - var action = new ServiceAction - { - Name = "RegisterDevice" - }; - - action.ArgumentList.Add(new Argument - { - Name = "RegistrationReqMsg", - Direction = "in" - }); - - action.ArgumentList.Add(new Argument - { - Name = "RegistrationRespMsg", - Direction = "out" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetValidationSucceededUpdateID". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetGetValidationSucceededUpdateID() - { - var action = new ServiceAction - { - Name = "GetValidationSucceededUpdateID" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ValidationSucceededUpdateID", - Direction = "out" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetGetAuthorizationDeniedUpdateID". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetGetAuthorizationDeniedUpdateID() - { - var action = new ServiceAction - { - Name = "GetAuthorizationDeniedUpdateID" - }; - - action.ArgumentList.Add(new Argument - { - Name = "AuthorizationDeniedUpdateID", - Direction = "out" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetValidationRevokedUpdateID". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetGetValidationRevokedUpdateID() - { - var action = new ServiceAction - { - Name = "GetValidationRevokedUpdateID" - }; - - action.ArgumentList.Add(new Argument - { - Name = "ValidationRevokedUpdateID", - Direction = "out" - }); - - return action; - } - - /// <summary> - /// Returns the action details for "GetAuthorizationGrantedUpdateID". - /// </summary> - /// <returns>The <see cref="ServiceAction"/>.</returns> - private static ServiceAction GetGetAuthorizationGrantedUpdateID() - { - var action = new ServiceAction - { - Name = "GetAuthorizationGrantedUpdateID" - }; - - action.ArgumentList.Add(new Argument - { - Name = "AuthorizationGrantedUpdateID", - Direction = "out" - }); - - return action; - } - } -} diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs deleted file mode 100644 index 18fa19650..000000000 --- a/Emby.Dlna/PlayTo/Device.cs +++ /dev/null @@ -1,1264 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Security; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; -using Emby.Dlna.Common; -using Emby.Dlna.Ssdp; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.PlayTo -{ - public class Device : IDisposable - { - private readonly IHttpClientFactory _httpClientFactory; - - private readonly ILogger _logger; - - private readonly object _timerLock = new object(); - private Timer? _timer; - private int _muteVol; - private int _volume; - private DateTime _lastVolumeRefresh; - private bool _volumeRefreshActive; - private int _connectFailureCount; - private bool _disposed; - - public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger) - { - Properties = deviceProperties; - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - public event EventHandler<PlaybackStartEventArgs>? PlaybackStart; - - public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress; - - public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped; - - public event EventHandler<MediaChangedEventArgs>? MediaChanged; - - public DeviceInfo Properties { get; set; } - - public bool IsMuted { get; set; } - - public int Volume - { - get - { - RefreshVolumeIfNeeded().GetAwaiter().GetResult(); - return _volume; - } - - set => _volume = value; - } - - public TimeSpan? Duration { get; set; } - - public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0); - - public TransportState TransportState { get; private set; } - - public bool IsPlaying => TransportState == TransportState.PLAYING; - - public bool IsPaused => TransportState == TransportState.PAUSED_PLAYBACK; - - public bool IsStopped => TransportState == TransportState.STOPPED; - - public Action? OnDeviceUnavailable { get; set; } - - private TransportCommands? AvCommands { get; set; } - - private TransportCommands? RendererCommands { get; set; } - - public UBaseObject? CurrentMediaInfo { get; private set; } - - public void Start() - { - _logger.LogDebug("Dlna Device.Start"); - _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite); - } - - private Task RefreshVolumeIfNeeded() - { - if (_volumeRefreshActive - && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5)) - { - _lastVolumeRefresh = DateTime.UtcNow; - return RefreshVolume(); - } - - return Task.CompletedTask; - } - - private async Task RefreshVolume(CancellationToken cancellationToken = default) - { - if (_disposed) - { - return; - } - - try - { - await GetVolume(cancellationToken).ConfigureAwait(false); - await GetMute(cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating device volume info for {DeviceName}", Properties.Name); - } - } - - private void RestartTimer(bool immediate = false) - { - lock (_timerLock) - { - if (_disposed) - { - return; - } - - _volumeRefreshActive = true; - - var time = immediate ? 100 : 10000; - _timer?.Change(time, Timeout.Infinite); - } - } - - /// <summary> - /// Restarts the timer in inactive mode. - /// </summary> - private void RestartTimerInactive() - { - lock (_timerLock) - { - if (_disposed) - { - return; - } - - _volumeRefreshActive = false; - - _timer?.Change(Timeout.Infinite, Timeout.Infinite); - } - } - - public Task VolumeDown(CancellationToken cancellationToken) - { - var sendVolume = Math.Max(Volume - 5, 0); - - return SetVolume(sendVolume, cancellationToken); - } - - public Task VolumeUp(CancellationToken cancellationToken) - { - var sendVolume = Math.Min(Volume + 5, 100); - - return SetVolume(sendVolume, cancellationToken); - } - - public Task ToggleMute(CancellationToken cancellationToken) - { - if (IsMuted) - { - return Unmute(cancellationToken); - } - - return Mute(cancellationToken); - } - - public async Task Mute(CancellationToken cancellationToken) - { - var success = await SetMute(true, cancellationToken).ConfigureAwait(true); - - if (!success) - { - await SetVolume(0, cancellationToken).ConfigureAwait(false); - } - } - - public async Task Unmute(CancellationToken cancellationToken) - { - var success = await SetMute(false, cancellationToken).ConfigureAwait(true); - - if (!success) - { - var sendVolume = _muteVol <= 0 ? 20 : _muteVol; - - await SetVolume(sendVolume, cancellationToken).ConfigureAwait(false); - } - } - - private DeviceService? GetServiceRenderingControl() - { - var services = Properties.Services; - - return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:RenderingControl:1", StringComparison.OrdinalIgnoreCase)) ?? - services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase)); - } - - private DeviceService? GetAvTransportService() - { - var services = Properties.Services; - - return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:AVTransport:1", StringComparison.OrdinalIgnoreCase)) ?? - services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:AVTransport", StringComparison.OrdinalIgnoreCase)); - } - - private async Task<bool> SetMute(bool mute, CancellationToken cancellationToken) - { - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); - if (command is null) - { - return false; - } - - var service = GetServiceRenderingControl(); - - if (service is null) - { - return false; - } - - _logger.LogDebug("Setting mute"); - var value = mute ? 1 : 0; - - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - IsMuted = mute; - - return true; - } - - /// <summary> - /// Sets volume on a scale of 0-100. - /// </summary> - /// <param name="value">The volume on a scale of 0-100.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - public async Task SetVolume(int value, CancellationToken cancellationToken) - { - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); - if (command is null) - { - return; - } - - var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service"); - - // Set it early and assume it will succeed - // Remote control will perform better - Volume = value; - - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task Seek(TimeSpan value, CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); - if (command is null) - { - return; - } - - 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"), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - RestartTimer(true); - } - - public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - url = url.Replace("&", "&", StringComparison.Ordinal); - - _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); - if (command is null) - { - return; - } - - var dictionary = new Dictionary<string, string> - { - { "CurrentURI", url }, - { "CurrentURIMetaData", CreateDidlMeta(metaData) } - }; - - 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: header, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - - try - { - await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); - } - catch - { - // Some devices will throw an error if you tell it to play when it's already playing - // Others won't - } - - RestartTimer(true); - } - - /* - * 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) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - url = url.Replace("&", "&", StringComparison.Ordinal); - - _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)); - if (command is null) - { - return; - } - - var dictionary = new Dictionary<string, string> - { - { "NextURI", url }, - { "NextURIMetaData", CreateDidlMeta(metaData) } - }; - - 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); - } - - private static string CreateDidlMeta(string value) - { - if (string.IsNullOrEmpty(value)) - { - return string.Empty; - } - - return SecurityElement.Escape(value); - } - - private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) - { - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play"); - if (command is null) - { - return Task.CompletedTask; - } - - var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); - return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - avCommands.BuildPost(command, service.ServiceType, 1), - cancellationToken: cancellationToken); - } - - public async Task SetPlay(CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - if (avCommands is null) - { - return; - } - - await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); - - RestartTimer(true); - } - - public async Task SetStop(CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); - if (command is null) - { - return; - } - - 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), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - RestartTimer(true); - } - - public async Task SetPause(CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); - if (command is null) - { - return; - } - - 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), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - TransportState = TransportState.PAUSED_PLAYBACK; - - RestartTimer(true); - } - - private async void TimerCallback(object? sender) - { - if (_disposed) - { - return; - } - - try - { - var cancellationToken = CancellationToken.None; - - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - if (avCommands is null) - { - return; - } - - var transportState = await GetTransportInfo(avCommands, cancellationToken).ConfigureAwait(false); - - if (_disposed) - { - return; - } - - if (transportState.HasValue) - { - // If we're not playing anything no need to get additional data - if (transportState.Value == TransportState.STOPPED) - { - UpdateMediaInfo(null, transportState.Value); - } - else - { - var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false); - - var currentObject = tuple.Track; - - if (tuple.Success && currentObject is null) - { - currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false); - } - - if (currentObject is not null) - { - UpdateMediaInfo(currentObject, transportState.Value); - } - } - - _connectFailureCount = 0; - - if (_disposed) - { - return; - } - - // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive - if (transportState.Value == TransportState.STOPPED) - { - RestartTimerInactive(); - } - else - { - RestartTimer(); - } - } - else - { - RestartTimerInactive(); - } - } - catch (Exception ex) - { - if (_disposed) - { - return; - } - - _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name); - - _connectFailureCount++; - - if (_connectFailureCount >= 3) - { - var action = OnDeviceUnavailable; - if (action is not null) - { - _logger.LogDebug("Disposing device due to loss of connection"); - action(); - return; - } - } - - RestartTimerInactive(); - } - } - - private async Task GetVolume(CancellationToken cancellationToken) - { - if (_disposed) - { - return; - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); - if (command is null) - { - return; - } - - var service = GetServiceRenderingControl(); - - if (service is null) - { - return; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType), // null checked above - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return; - } - - var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i is not null); - var volumeValue = volume?.Value; - - if (string.IsNullOrWhiteSpace(volumeValue)) - { - return; - } - - Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture); - - if (Volume > 0) - { - _muteVol = Volume; - } - } - - private async Task GetMute(CancellationToken cancellationToken) - { - if (_disposed) - { - return; - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); - if (command is null) - { - return; - } - - var service = GetServiceRenderingControl(); - - if (service is null) - { - return; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType), // null checked above - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return; - } - - var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse") - .Select(i => i.Element("CurrentMute")) - .FirstOrDefault(i => i is not null); - - IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase); - } - - private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) - { - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); - if (command is null) - { - return null; - } - - var service = GetAvTransportService(); - if (service is null) - { - return null; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - avCommands.BuildPost(command, service.ServiceType), - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return null; - } - - var transportState = - result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i is not null); - - var transportStateValue = transportState?.Value; - - if (transportStateValue is not null - && Enum.TryParse(transportStateValue, true, out TransportState state)) - { - return state; - } - - return null; - } - - private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) - { - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); - if (command is null) - { - return null; - } - - var service = GetAvTransportService(); - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - if (rendererCommands is null) - { - return null; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands.BuildPost(command, service.ServiceType), - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return null; - } - - var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault(); - - if (track is null) - { - return null; - } - - var e = track.Element(UPnpNamespaces.Items) ?? track; - - var elementString = (string)e; - - if (!string.IsNullOrWhiteSpace(elementString)) - { - return UpnpContainer.Create(e); - } - - track = result.Document.Descendants("CurrentURI").FirstOrDefault(); - - if (track is null) - { - return null; - } - - e = track.Element(UPnpNamespaces.Items) ?? track; - - elementString = (string)e; - - if (!string.IsNullOrWhiteSpace(elementString)) - { - return new UBaseObject - { - Url = elementString - }; - } - - return null; - } - - 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) - { - return (false, null); - } - - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - if (rendererCommands is null) - { - return (false, null); - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands.BuildPost(command, service.ServiceType), - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return (false, null); - } - - var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i is not null); - var trackUri = trackUriElem?.Value; - - var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i is not null); - var duration = durationElem?.Value; - - if (!string.IsNullOrWhiteSpace(duration) - && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) - { - Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture); - } - else - { - Duration = null; - } - - var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i is not null); - var position = positionElem?.Value; - - if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) - { - Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture); - } - - var track = result.Document.Descendants("TrackMetaData").FirstOrDefault(); - - if (track is null) - { - // If track is null, some vendors do this, use GetMediaInfo instead. - return (true, null); - } - - var trackString = (string)track; - - if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) - { - return (true, null); - } - - XElement? uPnpResponse = null; - - try - { - uPnpResponse = ParseResponse(trackString); - } - catch (Exception ex) - { - _logger.LogError(ex, "Uncaught exception while parsing xml"); - } - - if (uPnpResponse is null) - { - _logger.LogError("Failed to parse xml: \n {Xml}", trackString); - return (true, null); - } - - var e = uPnpResponse.Element(UPnpNamespaces.Items); - - var uTrack = CreateUBaseObject(e, trackUri); - - return (true, uTrack); - } - - private XElement? ParseResponse(string xml) - { - // Handle different variations sent back by devices. - try - { - return XElement.Parse(xml); - } - catch (XmlException) - { - } - - // first try to add a root node with a dlna namespace. - try - { - return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>") - .Descendants() - .First(); - } - catch (XmlException) - { - } - - // some devices send back invalid xml - try - { - return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal)); - } - catch (XmlException) - { - } - - return null; - } - - private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri) - { - ArgumentNullException.ThrowIfNull(container); - - var url = container.GetValue(UPnpNamespaces.Res); - - if (string.IsNullOrWhiteSpace(url)) - { - url = trackUri; - } - - return new UBaseObject - { - Id = container.GetAttributeValue(UPnpNamespaces.Id), - ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), - Title = container.GetValue(UPnpNamespaces.Title), - IconUrl = container.GetValue(UPnpNamespaces.Artwork), - SecondText = string.Empty, - Url = url, - ProtocolInfo = GetProtocolInfo(container), - MetaData = container.ToString() - }; - } - - private static string[] GetProtocolInfo(XElement container) - { - ArgumentNullException.ThrowIfNull(container); - - var resElement = container.Element(UPnpNamespaces.Res); - - var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo); - - if (info is not null && !string.IsNullOrWhiteSpace(info.Value)) - { - return info.Value.Split(':'); - } - - return new string[4]; - } - - private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken) - { - if (AvCommands is not null) - { - return AvCommands; - } - - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - var avService = GetAvTransportService(); - if (avService is null) - { - return null; - } - - string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - - var httpClient = new DlnaHttpClient(_logger, _httpClientFactory); - - var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); - if (document is null) - { - return null; - } - - AvCommands = TransportCommands.Create(document); - return AvCommands; - } - - private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken) - { - if (RendererCommands is not null) - { - return RendererCommands; - } - - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - var avService = GetServiceRenderingControl(); - ArgumentNullException.ThrowIfNull(avService); - - string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - - var httpClient = new DlnaHttpClient(_logger, _httpClientFactory); - _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); - var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); - if (document is null) - { - return null; - } - - RendererCommands = TransportCommands.Create(document); - return RendererCommands; - } - - private string NormalizeUrl(string baseUrl, string url) - { - // If it's already a complete url, don't stick anything onto the front of it - if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return url; - } - - if (!url.Contains('/', StringComparison.Ordinal)) - { - url = "/dmr/" + url; - } - - if (!url.StartsWith('/')) - { - url = "/" + url; - } - - return baseUrl + url; - } - - public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) - { - var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory); - - var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); - if (document is null) - { - return null; - } - - var friendlyNames = new List<string>(); - - var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault(); - if (name is not null && !string.IsNullOrWhiteSpace(name.Value)) - { - friendlyNames.Add(name.Value); - } - - var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault(); - if (room is not null && !string.IsNullOrWhiteSpace(room.Value)) - { - friendlyNames.Add(room.Value); - } - - var deviceProperties = new DeviceInfo() - { - Name = string.Join(' ', friendlyNames), - BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port) - }; - - var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault(); - if (model is not null) - { - deviceProperties.ModelName = model.Value; - } - - var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault(); - if (modelNumber is not null) - { - deviceProperties.ModelNumber = modelNumber.Value; - } - - var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault(); - if (uuid is not null) - { - deviceProperties.UUID = uuid.Value; - } - - var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault(); - if (manufacturer is not null) - { - deviceProperties.Manufacturer = manufacturer.Value; - } - - var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault(); - if (manufacturerUrl is not null) - { - deviceProperties.ManufacturerUrl = manufacturerUrl.Value; - } - - var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault(); - if (presentationUrl is not null) - { - deviceProperties.PresentationUrl = presentationUrl.Value; - } - - var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault(); - if (modelUrl is not null) - { - deviceProperties.ModelUrl = modelUrl.Value; - } - - var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault(); - if (serialNumber is not null) - { - deviceProperties.SerialNumber = serialNumber.Value; - } - - var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault(); - if (modelDescription is not null) - { - deviceProperties.ModelDescription = modelDescription.Value; - } - - var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault(); - if (icon is not null) - { - deviceProperties.Icon = CreateIcon(icon); - } - - foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList"))) - { - if (services is null) - { - continue; - } - - var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service")); - if (servicesList is null) - { - continue; - } - - foreach (var element in servicesList) - { - var service = Create(element); - - if (service is not null) - { - deviceProperties.Services.Add(service); - } - } - } - - return new Device(deviceProperties, httpClientFactory, logger); - } - - private static DeviceIcon CreateIcon(XElement element) - { - ArgumentNullException.ThrowIfNull(element); - - var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width")); - var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height")); - - _ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue); - _ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue); - - return new DeviceIcon - { - Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty, - Height = heightValue, - MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty, - Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty, - Width = widthValue - }; - } - - private static DeviceService Create(XElement element) - => new DeviceService() - { - ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty, - EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty, - ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty, - ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty, - ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty - }; - - private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state) - { - TransportState = state; - - var previousMediaInfo = CurrentMediaInfo; - CurrentMediaInfo = mediaInfo; - - if (mediaInfo is null) - { - if (previousMediaInfo is not null) - { - OnPlaybackStop(previousMediaInfo); - } - } - else if (previousMediaInfo is null) - { - if (state != TransportState.STOPPED) - { - OnPlaybackStart(mediaInfo); - } - } - else if (mediaInfo.Equals(previousMediaInfo)) - { - OnPlaybackProgress(mediaInfo); - } - else - { - OnMediaChanged(previousMediaInfo, mediaInfo); - } - } - - private void OnPlaybackStart(UBaseObject mediaInfo) - { - if (string.IsNullOrWhiteSpace(mediaInfo.Url)) - { - return; - } - - PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo)); - } - - private void OnPlaybackProgress(UBaseObject mediaInfo) - { - if (string.IsNullOrWhiteSpace(mediaInfo.Url)) - { - return; - } - - PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo)); - } - - private void OnPlaybackStop(UBaseObject mediaInfo) - { - PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo)); - } - - private void OnMediaChanged(UBaseObject old, UBaseObject newMedia) - { - MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia)); - } - - /// <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) - { - _timer?.Dispose(); - _timer = null; - Properties = null!; - } - - _disposed = true; - } - - /// <inheritdoc /> - public override string ToString() - { - return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl); - } - } -} diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs deleted file mode 100644 index 2acfff4eb..000000000 --- a/Emby.Dlna/PlayTo/DeviceInfo.cs +++ /dev/null @@ -1,66 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using Emby.Dlna.Common; -using MediaBrowser.Model.Dlna; - -namespace Emby.Dlna.PlayTo -{ - public class DeviceInfo - { - private readonly List<DeviceService> _services = new List<DeviceService>(); - private string _baseUrl = string.Empty; - - public DeviceInfo() - { - Name = "Generic Device"; - } - - public string UUID { get; set; } - - public string Name { get; set; } - - public string ModelName { get; set; } - - public string ModelNumber { get; set; } - - public string ModelDescription { get; set; } - - public string ModelUrl { get; set; } - - public string Manufacturer { get; set; } - - public string SerialNumber { get; set; } - - public string ManufacturerUrl { get; set; } - - public string PresentationUrl { get; set; } - - public string BaseUrl - { - get => _baseUrl; - set => _baseUrl = value; - } - - public DeviceIcon Icon { get; set; } - - public List<DeviceService> Services => _services; - - public DeviceIdentification ToDeviceIdentification() - { - return new DeviceIdentification - { - Manufacturer = Manufacturer, - ModelName = ModelName, - ModelNumber = ModelNumber, - FriendlyName = Name, - ManufacturerUrl = ManufacturerUrl, - ModelUrl = ModelUrl, - ModelDescription = ModelDescription, - SerialNumber = SerialNumber - }; - } - } -} diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs deleted file mode 100644 index 255c51f19..000000000 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ /dev/null @@ -1,137 +0,0 @@ -#pragma warning disable CS1591 - -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; -using System.Xml.Linq; -using Emby.Dlna.Common; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.PlayTo -{ - /// <summary> - /// Http client for Dlna PlayTo function. - /// </summary> - public partial class DlnaHttpClient - { - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - - public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory) - { - _logger = logger; - _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 - if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return serviceUrl; - } - - if (!serviceUrl.StartsWith('/')) - { - serviceUrl = "/" + serviceUrl; - } - - return baseUrl + serviceUrl; - } - - private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var client = _httpClientFactory.CreateClient(NamedClient.Dlna); - using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) - { - try - { - return await XDocument.LoadAsync( - stream, - LoadOptions.None, - cancellationToken).ConfigureAwait(false); - } - catch (XmlException) - { - // 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); - - // 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; - } - } - } - } - - public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken) - { - using var request = new HttpRequestMessage(HttpMethod.Get, url); - - // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon - return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - } - - public async Task<XDocument?> SendCommandAsync( - string baseUrl, - DeviceService service, - string command, - string postData, - string? header = null, - CancellationToken cancellationToken = default) - { - using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl)) - { - Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml) - }; - - request.Headers.TryAddWithoutValidation( - "SOAPACTION", - string.Format( - CultureInfo.InvariantCulture, - "\"{0}#{1}\"", - service.ServiceType, - command)); - request.Headers.Pragma.ParseAdd("no-cache"); - - if (!string.IsNullOrEmpty(header)) - { - request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header); - } - - // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon - return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs deleted file mode 100644 index 0f7a524d6..000000000 --- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class MediaChangedEventArgs : EventArgs - { - public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo) - { - OldMediaInfo = oldMediaInfo; - NewMediaInfo = newMediaInfo; - } - - public UBaseObject OldMediaInfo { get; set; } - - public UBaseObject NewMediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs deleted file mode 100644 index f70ebf3eb..000000000 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ /dev/null @@ -1,980 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Emby.Dlna.Didl; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Events; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Session; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; -using Photo = MediaBrowser.Controller.Entities.Photo; - -namespace Emby.Dlna.PlayTo -{ - public class PlayToController : ISessionController, IDisposable - { - private readonly SessionInfo _session; - private readonly ISessionManager _sessionManager; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IDlnaManager _dlnaManager; - private readonly IUserManager _userManager; - 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 string _serverAddress; - private readonly string? _accessToken; - - private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); - private Device _device; - private int _currentPlaylistIndex; - - private bool _disposed; - - public PlayToController( - SessionInfo session, - ISessionManager sessionManager, - ILibraryManager libraryManager, - ILogger logger, - IDlnaManager dlnaManager, - IUserManager userManager, - IImageProcessor imageProcessor, - string serverAddress, - string? accessToken, - IDeviceDiscovery deviceDiscovery, - IUserDataManager userDataManager, - ILocalizationManager localization, - IMediaSourceManager mediaSourceManager, - IMediaEncoder mediaEncoder, - Device device) - { - _session = session; - _sessionManager = sessionManager; - _libraryManager = libraryManager; - _logger = logger; - _dlnaManager = dlnaManager; - _userManager = userManager; - _imageProcessor = imageProcessor; - _serverAddress = serverAddress; - _accessToken = accessToken; - _deviceDiscovery = deviceDiscovery; - _userDataManager = userDataManager; - _localization = localization; - _mediaSourceManager = mediaSourceManager; - _mediaEncoder = mediaEncoder; - - _device = device; - _device.OnDeviceUnavailable = OnDeviceUnavailable; - _device.PlaybackStart += OnDevicePlaybackStart; - _device.PlaybackProgress += OnDevicePlaybackProgress; - _device.PlaybackStopped += OnDevicePlaybackStopped; - _device.MediaChanged += OnDeviceMediaChanged; - - _device.Start(); - - _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. - */ - private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken) - { - if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1) - { - // The current playing item is indeed in the play list and we are not yet at the end of the playlist. - var nextItemIndex = currentPlayListItemIndex + 1; - var nextItem = _playlist[nextItemIndex]; - - // Send the SetNextAvTransport message. - await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false); - } - } - - private void OnDeviceUnavailable() - { - try - { - _sessionManager.ReportSessionEnded(_session.Id); - } - catch (Exception ex) - { - // Could throw if the session is already gone - _logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id); - } - } - - private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e) - { - var info = e.Argument; - - if (!_disposed - && 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) - && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1))) - { - OnDeviceUnavailable(); - } - } - - private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e) - { - if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url)) - { - return; - } - - try - { - var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager); - if (streamInfo.Item is not null) - { - var positionTicks = GetProgressPositionTicks(streamInfo); - - await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false); - } - - streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager); - if (streamInfo.Item is null) - { - return; - } - - var newItemProgress = GetProgressInfo(streamInfo); - - await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false); - - // Send a message to the DLNA device to notify what is the next track in the playlist. - var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId)); - if (currentItemIndex >= 0) - { - _currentPlaylistIndex = currentItemIndex; - } - - await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e) - { - if (_disposed) - { - return; - } - - try - { - var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager); - - if (streamInfo.Item is null) - { - return; - } - - var positionTicks = GetProgressPositionTicks(streamInfo); - - await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false); - - var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false); - - var duration = mediaSource is null - ? _device.Duration?.Ticks - : mediaSource.RunTimeTicks; - - var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0; - - if (!playedToCompletion && duration.HasValue && positionTicks.HasValue) - { - double percent = positionTicks.Value; - percent /= duration.Value; - - playedToCompletion = Math.Abs(1 - percent) <= .1; - } - - if (playedToCompletion) - { - await SetPlaylistIndex(_currentPlaylistIndex + 1).ConfigureAwait(false); - } - else - { - _playlist.Clear(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting playback stopped"); - } - } - - private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks) - { - try - { - await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo - { - ItemId = streamInfo.ItemId, - SessionId = _session.Id, - PositionTicks = positionTicks, - MediaSourceId = streamInfo.MediaSourceId - }).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e) - { - if (_disposed) - { - return; - } - - try - { - var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var progress = GetProgressInfo(info); - - await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e) - { - if (_disposed) - { - return; - } - - try - { - var mediaUrl = e.MediaInfo.Url; - - if (string.IsNullOrWhiteSpace(mediaUrl)) - { - return; - } - - var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var progress = GetProgressInfo(info); - - await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private long? GetProgressPositionTicks(StreamParams info) - { - var ticks = _device.Position.Ticks; - - if (!EnableClientSideSeek(info)) - { - ticks += info.StartPositionTicks; - } - - return ticks; - } - - private PlaybackStartInfo GetProgressInfo(StreamParams info) - { - return new PlaybackStartInfo - { - ItemId = info.ItemId, - SessionId = _session.Id, - PositionTicks = GetProgressPositionTicks(info), - IsMuted = _device.IsMuted, - IsPaused = _device.IsPaused, - MediaSourceId = info.MediaSourceId, - AudioStreamIndex = info.AudioStreamIndex, - SubtitleStreamIndex = info.SubtitleStreamIndex, - VolumeLevel = _device.Volume, - - CanSeek = true, - - PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode - }; - } - - public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) - { - _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand); - - var user = command.ControllingUserId.Equals(default) - ? null : - _userManager.GetUserById(command.ControllingUserId); - - var items = new List<BaseItem>(); - foreach (var id in command.ItemIds) - { - AddItemFromId(id, items); - } - - var startIndex = command.StartIndex ?? 0; - int len = items.Count - startIndex; - if (startIndex > 0) - { - items = items.GetRange(startIndex, len); - } - - var playlist = new PlaylistItem[len]; - - // Not nullable enabled - so this is required. - playlist[0] = CreatePlaylistItem( - items[0], - user, - command.StartPositionTicks ?? 0, - command.MediaSourceId ?? string.Empty, - command.AudioStreamIndex, - command.SubtitleStreamIndex); - - for (int i = 1; i < len; i++) - { - playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null); - } - - _logger.LogDebug("{0} - Playlist created", _session.DeviceName); - - if (command.PlayCommand == PlayCommand.PlayLast) - { - _playlist.AddRange(playlist); - } - - if (command.PlayCommand == PlayCommand.PlayNext) - { - _playlist.AddRange(playlist); - } - - if (!command.ControllingUserId.Equals(default)) - { - _sessionManager.LogSessionActivity( - _session.Client, - _session.ApplicationVersion, - _session.DeviceId, - _session.DeviceName, - _session.RemoteEndPoint, - user); - } - - return PlayItems(playlist, cancellationToken); - } - - private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) - { - switch (command.Command) - { - case PlaystateCommand.Stop: - _playlist.Clear(); - return _device.SetStop(CancellationToken.None); - - case PlaystateCommand.Pause: - return _device.SetPause(CancellationToken.None); - - case PlaystateCommand.Unpause: - return _device.SetPlay(CancellationToken.None); - - case PlaystateCommand.PlayPause: - return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None); - - case PlaystateCommand.Seek: - return Seek(command.SeekPositionTicks ?? 0); - - case PlaystateCommand.NextTrack: - return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken); - - case PlaystateCommand.PreviousTrack: - return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken); - } - - return Task.CompletedTask; - } - - private async Task Seek(long newPosition) - { - var media = _device.CurrentMediaInfo; - - if (media is not null) - { - var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null && !EnableClientSideSeek(info)) - { - var user = _session.UserId.Equals(default) - ? null - : _userManager.GetUserById(_session.UserId); - var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex); - - await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); - - // Send a message to the DLNA device to notify what is the next track in the play list. - var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); - await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false); - - return; - } - - await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); - } - } - - private bool EnableClientSideSeek(StreamParams info) - { - return info.IsDirectStream; - } - - private bool EnableClientSideSeek(StreamInfo info) - { - return info.IsDirectStream; - } - - private void AddItemFromId(Guid id, List<BaseItem> list) - { - var item = _libraryManager.GetItemById(id); - if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video) - { - list.Add(item); - } - } - - private PlaylistItem CreatePlaylistItem( - BaseItem item, - User? user, - long startPostionTicks, - string? mediaSourceId, - int? audioStreamIndex, - int? subtitleStreamIndex) - { - var deviceInfo = _device.Properties; - - var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ?? - _dlnaManager.GetDefaultProfile(); - - var mediaSources = item is IHasMediaSources - ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray() - : Array.Empty<MediaSourceInfo>(); - - var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex); - playlistItem.StreamInfo.StartPositionTicks = startPostionTicks; - - playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken)); - - var itemXml = new DidlBuilder( - profile, - user, - _imageProcessor, - _serverAddress, - _accessToken, - _userDataManager, - _localization, - _mediaSourceManager, - _logger, - _mediaEncoder, - _libraryManager) - .GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo); - - playlistItem.Didl = itemXml; - - return playlistItem; - } - - private string? GetDlnaHeaders(PlaylistItem item) - { - var profile = item.Profile; - var streamInfo = item.StreamInfo; - - if (streamInfo.MediaType == DlnaProfileType.Audio) - { - return ContentFeatureBuilder.BuildAudioHeader( - profile, - streamInfo.Container, - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetAudioBitrate, - streamInfo.TargetAudioSampleRate, - streamInfo.TargetAudioChannels, - streamInfo.TargetAudioBitDepth, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TranscodeSeekInfo); - } - - if (streamInfo.MediaType == DlnaProfileType.Video) - { - var list = ContentFeatureBuilder.BuildVideoHeader( - profile, - streamInfo.Container, - streamInfo.TargetVideoCodec.FirstOrDefault(), - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetWidth, - streamInfo.TargetHeight, - streamInfo.TargetVideoBitDepth, - streamInfo.TargetVideoBitrate, - streamInfo.TargetTimestamp, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TargetVideoProfile, - streamInfo.TargetVideoRangeType, - streamInfo.TargetVideoLevel, - streamInfo.TargetFramerate ?? 0, - streamInfo.TargetPacketLength, - streamInfo.TranscodeSeekInfo, - streamInfo.IsTargetAnamorphic, - streamInfo.IsTargetInterlaced, - streamInfo.TargetRefFrames, - streamInfo.TargetVideoStreamCount, - streamInfo.TargetAudioStreamCount, - streamInfo.TargetVideoCodecTag, - streamInfo.IsTargetAVC); - - return list.FirstOrDefault(); - } - - return null; - } - - private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) - { - if (item.MediaType == MediaType.Video) - { - return new PlaylistItem - { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions - { - ItemId = item.Id, - MediaSources = mediaSources, - Profile = profile, - DeviceId = deviceId, - MaxBitrate = profile.MaxStreamingBitrate, - MediaSourceId = mediaSourceId, - AudioStreamIndex = audioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex - }), - - Profile = profile - }; - } - - if (item.MediaType == MediaType.Audio) - { - return new PlaylistItem - { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions - { - ItemId = item.Id, - MediaSources = mediaSources, - Profile = profile, - DeviceId = deviceId, - MaxBitrate = profile.MaxStreamingBitrate, - MediaSourceId = mediaSourceId - }), - - Profile = profile - }; - } - - if (item.MediaType == MediaType.Photo) - { - return PlaylistItemFactory.Create((Photo)item, profile); - } - - throw new ArgumentException("Unrecognized item type."); - } - - /// <summary> - /// Plays the items. - /// </summary> - /// <param name="items">The items.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns><c>true</c> on success.</returns> - private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default) - { - _playlist.Clear(); - _playlist.AddRange(items); - _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count); - - await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false); - return true; - } - - private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default) - { - if (index < 0 || index >= _playlist.Count) - { - _playlist.Clear(); - await _device.SetStop(cancellationToken).ConfigureAwait(false); - return; - } - - _currentPlaylistIndex = index; - var currentitem = _playlist[index]; - - await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false); - - // Send a message to the DLNA device to notify what is the next track in the play list. - await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false); - - var streamInfo = currentitem.StreamInfo; - if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo)) - { - await SeekAfterTransportChange(streamInfo.StartPositionTicks, CancellationToken.None).ConfigureAwait(false); - } - } - - /// <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) - { - _device.PlaybackStart -= OnDevicePlaybackStart; - _device.PlaybackProgress -= OnDevicePlaybackProgress; - _device.PlaybackStopped -= OnDevicePlaybackStopped; - _device.MediaChanged -= OnDeviceMediaChanged; - _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; - _device.OnDeviceUnavailable = null; - _device.Dispose(); - } - - _disposed = true; - } - - private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) - { - switch (command.Name) - { - case GeneralCommandType.VolumeDown: - return _device.VolumeDown(cancellationToken); - case GeneralCommandType.VolumeUp: - return _device.VolumeUp(cancellationToken); - case GeneralCommandType.Mute: - return _device.Mute(cancellationToken); - case GeneralCommandType.Unmute: - return _device.Unmute(cancellationToken); - case GeneralCommandType.ToggleMute: - return _device.ToggleMute(cancellationToken); - case GeneralCommandType.SetAudioStreamIndex: - if (command.Arguments.TryGetValue("Index", out string? index)) - { - if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return SetAudioStreamIndex(val); - } - - throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied."); - } - - throw new ArgumentException("SetAudioStreamIndex argument cannot be null"); - case GeneralCommandType.SetSubtitleStreamIndex: - if (command.Arguments.TryGetValue("Index", out index)) - { - if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return SetSubtitleStreamIndex(val); - } - - throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied."); - } - - throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); - case GeneralCommandType.SetVolume: - if (command.Arguments.TryGetValue("Volume", out string? vol)) - { - if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume)) - { - return _device.SetVolume(volume, cancellationToken); - } - - throw new ArgumentException("Unsupported volume value supplied."); - } - - throw new ArgumentException("Volume argument cannot be null"); - default: - return Task.CompletedTask; - } - } - - private async Task SetAudioStreamIndex(int? newIndex) - { - var media = _device.CurrentMediaInfo; - - if (media is not null) - { - var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var newPosition = GetProgressPositionTicks(info) ?? 0; - - var user = _session.UserId.Equals(default) - ? null - : _userManager.GetUserById(_session.UserId); - var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex); - - await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); - - // Send a message to the DLNA device to notify what is the next track in the play list. - var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); - await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false); - - if (EnableClientSideSeek(newItem.StreamInfo)) - { - await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); - } - } - } - } - - private async Task SetSubtitleStreamIndex(int? newIndex) - { - var media = _device.CurrentMediaInfo; - - if (media is not null) - { - var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var newPosition = GetProgressPositionTicks(info) ?? 0; - - var user = _session.UserId.Equals(default) - ? null - : _userManager.GetUserById(_session.UserId); - var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex); - - await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); - - // Send a message to the DLNA device to notify what is the next track in the play list. - var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); - await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false); - - if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0) - { - await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); - } - } - } - } - - private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken) - { - const int MaxWait = 15000000; - const int Interval = 500; - - var currentWait = 0; - while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait) - { - await Task.Delay(Interval, cancellationToken).ConfigureAwait(false); - currentWait += Interval; - } - - await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false); - } - - private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name) - { - var value = values.GetValueOrDefault(name); - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return null; - } - - private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name) - { - var value = values.GetValueOrDefault(name); - - if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return 0; - } - - /// <inheritdoc /> - public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - return name switch - { - 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; - - public Guid ItemId { get; set; } - - public bool IsDirectStream { get; set; } - - public long StartPositionTicks { get; set; } - - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public string? DeviceProfileId { get; set; } - - public string? DeviceId { get; set; } - - public string? MediaSourceId { get; set; } - - public string? LiveStreamId { get; set; } - - public BaseItem? Item { get; set; } - - public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken) - { - if (_mediaSource is not null) - { - return _mediaSource; - } - - if (Item is not IHasMediaSources) - { - return null; - } - - if (_mediaSourceManager is not null) - { - _mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); - } - - return _mediaSource; - } - - private static Guid GetItemId(string url) - { - ArgumentException.ThrowIfNullOrEmpty(url); - - var parts = url.Split('/'); - - for (var i = 0; i < parts.Length - 1; i++) - { - var part = parts[i]; - - if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) - || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) - { - if (Guid.TryParse(parts[i + 1], out var result)) - { - return result; - } - } - } - - return default; - } - - public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager) - { - ArgumentException.ThrowIfNullOrEmpty(url); - - var request = new StreamParams - { - ItemId = GetItemId(url) - }; - - if (request.ItemId.Equals(default)) - { - return request; - } - - var index = url.IndexOf('?', StringComparison.Ordinal); - if (index == -1) - { - return request; - } - - var query = url.Substring(index + 1); - Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); - - request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId"); - request.DeviceId = values.GetValueOrDefault("DeviceId"); - request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); - request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); - request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); - request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); - request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); - request.StartPositionTicks = GetLongValue(values, "StartPositionTicks"); - - request.Item = libraryManager.GetItemById(request.ItemId); - - request._mediaSourceManager = mediaSourceManager; - - return request; - } - } - } -} diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs deleted file mode 100644 index b05e0a095..000000000 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ /dev/null @@ -1,258 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Events; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -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 MediaBrowser.Model.Session; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.PlayTo -{ - public sealed class PlayToManager : IDisposable - { - private readonly ILogger _logger; - private readonly ISessionManager _sessionManager; - - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; - private readonly IServerApplicationHost _appHost; - private readonly IImageProcessor _imageProcessor; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IUserDataManager _userDataManager; - private readonly ILocalizationManager _localization; - - private readonly IDeviceDiscovery _deviceDiscovery; - 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; - - 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) - { - _logger = logger; - _sessionManager = sessionManager; - _libraryManager = libraryManager; - _userManager = userManager; - _dlnaManager = dlnaManager; - _appHost = appHost; - _imageProcessor = imageProcessor; - _deviceDiscovery = deviceDiscovery; - _httpClientFactory = httpClientFactory; - _userDataManager = userDataManager; - _localization = localization; - _mediaSourceManager = mediaSourceManager; - _mediaEncoder = mediaEncoder; - } - - public void Start() - { - _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; - } - - private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e) - { - if (_disposed) - { - return; - } - - var info = e.Argument; - - if (!info.Headers.TryGetValue("USN", out string? usn)) - { - usn = string.Empty; - } - - if (!info.Headers.TryGetValue("NT", out string? nt)) - { - nt = string.Empty; - } - - // It has to report that it's a media renderer - if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase) - && !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - var cancellationToken = _disposeCancellationTokenSource.Token; - - await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - if (_disposed) - { - return; - } - - if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1)) - { - return; - } - - await AddDevice(info, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating PlayTo device."); - } - finally - { - _sessionLock.Release(); - } - } - - internal static string GetUuid(string usn) - { - const string UuidStr = "uuid:"; - const string UuidColonStr = "::"; - - var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase); - if (index == -1) - { - return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - - ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..]; - - index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - tmp = tmp[..index]; - } - - index = tmp.IndexOf('{'); - if (index != -1) - { - int endIndex = tmp.IndexOf('}'); - if (endIndex != -1) - { - tmp = tmp[(index + 1)..endIndex]; - } - } - - return tmp.ToString(); - } - - private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken) - { - var uri = info.Location; - _logger.LogDebug("Attempting to create PlayToController from location {0}", uri); - - if (info.Headers.TryGetValue("USN", out string? uuid)) - { - uuid = GetUuid(uuid); - } - else - { - uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - - var sessionInfo = await _sessionManager - .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null) - .ConfigureAwait(false); - - var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault(); - - if (controller is null) - { - var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); - if (device is null) - { - _logger.LogError("Ignoring device as xml response is invalid."); - return; - } - - string deviceName = device.Properties.Name; - - _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - - string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress); - - controller = new PlayToController( - sessionInfo, - _sessionManager, - _libraryManager, - _logger, - _dlnaManager, - _userManager, - _imageProcessor, - serverAddress, - null, - _deviceDiscovery, - _userDataManager, - _localization, - _mediaSourceManager, - _mediaEncoder, - device); - - sessionInfo.AddController(controller); - - var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ?? - _dlnaManager.GetDefaultProfile(); - - _sessionManager.ReportCapabilities(sessionInfo.Id, new ClientCapabilities - { - PlayableMediaTypes = profile.GetSupportedMediaTypes(), - - SupportedCommands = new[] - { - GeneralCommandType.VolumeDown, - GeneralCommandType.VolumeUp, - GeneralCommandType.Mute, - GeneralCommandType.Unmute, - GeneralCommandType.ToggleMute, - GeneralCommandType.SetVolume, - GeneralCommandType.SetAudioStreamIndex, - GeneralCommandType.SetSubtitleStreamIndex, - GeneralCommandType.PlayMediaSource - }, - - SupportsMediaControl = true - }); - - _logger.LogInformation("DLNA Session created for {0} - {1}", device.Properties.Name, device.Properties.ModelName); - } - } - - /// <inheritdoc /> - public void Dispose() - { - _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; - - try - { - _disposeCancellationTokenSource.Cancel(); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Error while disposing PlayToManager"); - } - - _sessionLock.Dispose(); - _disposeCancellationTokenSource.Dispose(); - - _disposed = true; - } - } -} diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs deleted file mode 100644 index c95d8b1e8..000000000 --- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class PlaybackProgressEventArgs : EventArgs - { - public PlaybackProgressEventArgs(UBaseObject mediaInfo) - { - MediaInfo = mediaInfo; - } - - public UBaseObject MediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs deleted file mode 100644 index 619c861ed..000000000 --- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class PlaybackStartEventArgs : EventArgs - { - public PlaybackStartEventArgs(UBaseObject mediaInfo) - { - MediaInfo = mediaInfo; - } - - public UBaseObject MediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs deleted file mode 100644 index d0ec25059..000000000 --- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class PlaybackStoppedEventArgs : EventArgs - { - public PlaybackStoppedEventArgs(UBaseObject mediaInfo) - { - MediaInfo = mediaInfo; - } - - public UBaseObject MediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs deleted file mode 100644 index 5056e69ae..000000000 --- a/Emby.Dlna/PlayTo/PlaylistItem.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using MediaBrowser.Model.Dlna; - -namespace Emby.Dlna.PlayTo -{ - public class PlaylistItem - { - public string StreamUrl { get; set; } - - public string Didl { get; set; } - - public StreamInfo StreamInfo { get; set; } - - public DeviceProfile Profile { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs deleted file mode 100644 index 53cd05cfd..000000000 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ /dev/null @@ -1,70 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.IO; -using System.Linq; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Session; - -namespace Emby.Dlna.PlayTo -{ - public static class PlaylistItemFactory - { - public static PlaylistItem Create(Photo item, DeviceProfile profile) - { - var playlistItem = new PlaylistItem - { - StreamInfo = new StreamInfo - { - ItemId = item.Id, - MediaType = DlnaProfileType.Photo, - DeviceProfile = profile - }, - - Profile = profile - }; - - var directPlay = profile.DirectPlayProfiles - .FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item)); - - if (directPlay is not null) - { - playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream; - playlistItem.StreamInfo.Container = Path.GetExtension(item.Path); - - return playlistItem; - } - - var transcodingProfile = profile.TranscodingProfiles - .FirstOrDefault(i => i.Type == DlnaProfileType.Photo); - - if (transcodingProfile is not null) - { - playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode; - playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.'); - } - - return playlistItem; - } - - private static bool IsSupported(DirectPlayProfile profile, Photo item) - { - var mediaPath = item.Path; - - if (profile.Container.Length > 0) - { - // Check container type - var mediaContainer = (Path.GetExtension(mediaPath) ?? string.Empty).TrimStart('.'); - - if (!profile.SupportsContainer(mediaContainer)) - { - return false; - } - } - - return true; - } - } -} diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs deleted file mode 100644 index 6b2096d9d..000000000 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ /dev/null @@ -1,181 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Xml.Linq; -using Emby.Dlna.Common; -using Emby.Dlna.Ssdp; - -namespace Emby.Dlna.PlayTo -{ - public class TransportCommands - { - private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; - - public List<StateVariable> StateVariables { get; } = new List<StateVariable>(); - - public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>(); - - public static TransportCommands Create(XDocument document) - { - var command = new TransportCommands(); - - var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList"); - - foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action")) - { - command.ServiceActions.Add(ServiceActionFromXml(container)); - } - - var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault(); - - if (stateValues is not null) - { - foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable")) - { - command.StateVariables.Add(FromXml(container)); - } - } - - return command; - } - - private static ServiceAction ServiceActionFromXml(XElement container) - { - var serviceAction = new ServiceAction - { - Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, - }; - - var argumentList = serviceAction.ArgumentList; - - foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument")) - { - argumentList.Add(ArgumentFromXml(arg)); - } - - return serviceAction; - } - - private static Argument ArgumentFromXml(XElement container) - { - ArgumentNullException.ThrowIfNull(container); - - return new Argument - { - Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, - Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty, - RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty - }; - } - - private static StateVariable FromXml(XElement container) - { - var allowedValues = Array.Empty<string>(); - var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList") - .FirstOrDefault(); - - if (element is not null) - { - var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue"); - - allowedValues = values.Select(child => child.Value).ToArray(); - } - - return new StateVariable - { - Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, - DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty, - AllowedValues = allowedValues - }; - } - - public string BuildPost(ServiceAction action, string xmlNamespace) - { - var stateString = string.Empty; - - foreach (var arg in action.ArgumentList) - { - if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) - { - continue; - } - - if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) - { - stateString += BuildArgumentXml(arg, "0"); - } - else - { - stateString += BuildArgumentXml(arg, null); - } - } - - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); - } - - public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "") - { - var stateString = string.Empty; - - foreach (var arg in action.ArgumentList) - { - if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) - { - continue; - } - - if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) - { - stateString += BuildArgumentXml(arg, "0"); - } - else - { - stateString += BuildArgumentXml(arg, value.ToString(), commandParameter); - } - } - - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); - } - - public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary) - { - var stateString = string.Empty; - - foreach (var arg in action.ArgumentList) - { - if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) - { - stateString += BuildArgumentXml(arg, "0"); - } - else if (dictionary.TryGetValue(arg.Name, out var argValue)) - { - stateString += BuildArgumentXml(arg, argValue); - } - else - { - stateString += BuildArgumentXml(arg, value.ToString()); - } - } - - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); - } - - private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "") - { - var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase)); - - if (state is not null) - { - var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ?? - (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value); - - return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue); - } - - return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value); - } - } -} diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs deleted file mode 100644 index 0d6a78438..000000000 --- a/Emby.Dlna/PlayTo/TransportState.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna.PlayTo -{ - /// <summary> - /// Core of the AVTransport service. It defines the conceptually top- - /// level state of the transport, for example, whether it is playing, recording, etc. - /// </summary> - public enum TransportState - { - STOPPED, - PLAYING, - TRANSITIONING, - PAUSED_PLAYBACK - } -} diff --git a/Emby.Dlna/PlayTo/UpnpContainer.cs b/Emby.Dlna/PlayTo/UpnpContainer.cs deleted file mode 100644 index 017d51e60..000000000 --- a/Emby.Dlna/PlayTo/UpnpContainer.cs +++ /dev/null @@ -1,25 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Xml.Linq; -using Emby.Dlna.Ssdp; - -namespace Emby.Dlna.PlayTo -{ - public class UpnpContainer : UBaseObject - { - public static UBaseObject Create(XElement container) - { - ArgumentNullException.ThrowIfNull(container); - - return new UBaseObject - { - Id = container.GetAttributeValue(UPnpNamespaces.Id), - ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), - Title = container.GetValue(UPnpNamespaces.Title), - IconUrl = container.GetValue(UPnpNamespaces.Artwork), - UpnpClass = container.GetValue(UPnpNamespaces.Class) - }; - } - } -} diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs deleted file mode 100644 index a8f451405..000000000 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ /dev/null @@ -1,63 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using Jellyfin.Data.Enums; - -namespace Emby.Dlna.PlayTo -{ - public class UBaseObject - { - public string Id { get; set; } - - public string ParentId { get; set; } - - public string Title { get; set; } - - public string SecondText { get; set; } - - public string IconUrl { get; set; } - - public string MetaData { get; set; } - - public string Url { get; set; } - - public IReadOnlyList<string> ProtocolInfo { get; set; } - - public string UpnpClass { get; set; } - - public string MediaType - { - get - { - var classType = UpnpClass ?? string.Empty; - - if (classType.Contains("Audio", StringComparison.Ordinal)) - { - return "Audio"; - } - - if (classType.Contains("Video", StringComparison.Ordinal)) - { - return "Video"; - } - - if (classType.Contains("image", StringComparison.Ordinal)) - { - return "Photo"; - } - - return null; - } - } - - public bool Equals(UBaseObject obj) - { - ArgumentNullException.ThrowIfNull(obj); - - return string.Equals(Id, obj.Id, StringComparison.Ordinal); - } - } -} diff --git a/Emby.Dlna/PlayTo/uPnpNamespaces.cs b/Emby.Dlna/PlayTo/uPnpNamespaces.cs deleted file mode 100644 index 5042d4493..000000000 --- a/Emby.Dlna/PlayTo/uPnpNamespaces.cs +++ /dev/null @@ -1,67 +0,0 @@ -#pragma warning disable CS1591 - -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public static class UPnpNamespaces - { - public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/"; - - public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - - public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0"; - - public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0"; - - public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - - public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1"; - - public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1"; - - public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1"; - - public static XName Containers { get; } = Ns + "container"; - - public static XName Items { get; } = Ns + "item"; - - public static XName Title { get; } = Dc + "title"; - - public static XName Creator { get; } = Dc + "creator"; - - public static XName Artist { get; } = UPnp + "artist"; - - public static XName Id { get; } = "id"; - - public static XName ParentId { get; } = "parentID"; - - public static XName Class { get; } = UPnp + "class"; - - public static XName Artwork { get; } = UPnp + "albumArtURI"; - - public static XName Description { get; } = Dc + "description"; - - public static XName LongDescription { get; } = UPnp + "longDescription"; - - public static XName Album { get; } = UPnp + "album"; - - public static XName Author { get; } = UPnp + "author"; - - public static XName Director { get; } = UPnp + "director"; - - public static XName PlayCount { get; } = UPnp + "playbackCount"; - - public static XName Tracknumber { get; } = UPnp + "originalTrackNumber"; - - public static XName Res { get; } = Ns + "res"; - - public static XName Duration { get; } = "duration"; - - public static XName ProtocolInfo { get; } = "protocolInfo"; - - public static XName ServiceStateTable { get; } = Svc + "serviceStateTable"; - - public static XName StateVariable { get; } = Svc + "stateVariable"; - } -} diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs deleted file mode 100644 index 54a0a87a8..000000000 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ /dev/null @@ -1,179 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using MediaBrowser.Model.Dlna; - -namespace Emby.Dlna.Profiles -{ - [System.Xml.Serialization.XmlRoot("Profile")] - public class DefaultProfile : DeviceProfile - { - public DefaultProfile() - { - Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - Name = "Generic Device"; - - ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*"; - - Manufacturer = "Jellyfin"; - ModelDescription = "UPnP/AV 1.0 Compliant Media Server"; - ModelName = "Jellyfin Server"; - ModelNumber = "01"; - ModelUrl = "https://github.com/jellyfin/jellyfin"; - ManufacturerUrl = "https://github.com/jellyfin/jellyfin"; - - AlbumArtPn = "JPEG_SM"; - - MaxAlbumArtHeight = 480; - MaxAlbumArtWidth = 480; - - MaxIconWidth = 48; - MaxIconHeight = 48; - - MaxStreamingBitrate = 140000000; - MaxStaticBitrate = 140000000; - MusicStreamingTranscodingBitrate = 192000; - - EnableAlbumArtInDidl = false; - - TranscodingProfiles = new[] - { - new TranscodingProfile - { - Container = "mp3", - AudioCodec = "mp3", - Type = DlnaProfileType.Audio - }, - - new TranscodingProfile - { - Container = "ts", - Type = DlnaProfileType.Video, - AudioCodec = "aac", - VideoCodec = "h264" - }, - - new TranscodingProfile - { - Container = "jpeg", - Type = DlnaProfileType.Photo - } - }; - - DirectPlayProfiles = new[] - { - new DirectPlayProfile - { - // play all - Container = string.Empty, - Type = DlnaProfileType.Video - }, - - new DirectPlayProfile - { - // play all - Container = string.Empty, - Type = DlnaProfileType.Audio - } - }; - - SubtitleProfiles = new[] - { - new SubtitleProfile - { - Format = "srt", - Method = SubtitleDeliveryMethod.External, - }, - - new SubtitleProfile - { - Format = "sub", - Method = SubtitleDeliveryMethod.External, - }, - - new SubtitleProfile - { - Format = "sup", - Method = SubtitleDeliveryMethod.External - }, - - new SubtitleProfile - { - Format = "srt", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "ass", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "ssa", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "smi", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "dvdsub", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "pgs", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "pgssub", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "sub", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "sup", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "subrip", - Method = SubtitleDeliveryMethod.Embed - }, - - new SubtitleProfile - { - Format = "vtt", - Method = SubtitleDeliveryMethod.Embed - } - }; - - ResponseProfiles = new[] - { - new ResponseProfile - { - Container = "m4v", - Type = DlnaProfileType.Video, - MimeType = "video/mp4" - } - }; - } - } -} diff --git a/Emby.Dlna/Profiles/Xml/Default.xml b/Emby.Dlna/Profiles/Xml/Default.xml deleted file mode 100644 index 9460f9d5a..000000000 --- a/Emby.Dlna/Profiles/Xml/Default.xml +++ /dev/null @@ -1,61 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Generic Device</Name> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="" type="Video" /> - <DirectPlayProfile container="" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles /> - <ResponseProfiles> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="External" /> - <SubtitleProfile format="sub" method="External" /> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="ass" method="Embed" /> - <SubtitleProfile format="ssa" method="Embed" /> - <SubtitleProfile format="smi" method="Embed" /> - <SubtitleProfile format="dvdsub" method="Embed" /> - <SubtitleProfile format="pgs" method="Embed" /> - <SubtitleProfile format="pgssub" method="Embed" /> - <SubtitleProfile format="sub" method="Embed" /> - <SubtitleProfile format="subrip" method="Embed" /> - <SubtitleProfile format="vtt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Denon AVR.xml b/Emby.Dlna/Profiles/Xml/Denon AVR.xml deleted file mode 100644 index 571786906..000000000 --- a/Emby.Dlna/Profiles/Xml/Denon AVR.xml +++ /dev/null @@ -1,68 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Denon AVR</Name> - <Identification> - <FriendlyName>Denon:\[AVR:.*</FriendlyName> - <Manufacturer>Denon</Manufacturer> - <Headers /> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles> - <CodecProfile type="Audio" container="flac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioSampleRate" value="96000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="External" /> - <SubtitleProfile format="sub" method="External" /> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="ass" method="Embed" /> - <SubtitleProfile format="ssa" method="Embed" /> - <SubtitleProfile format="smi" method="Embed" /> - <SubtitleProfile format="dvdsub" method="Embed" /> - <SubtitleProfile format="pgs" method="Embed" /> - <SubtitleProfile format="pgssub" method="Embed" /> - <SubtitleProfile format="sub" method="Embed" /> - <SubtitleProfile format="subrip" method="Embed" /> - <SubtitleProfile format="vtt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml deleted file mode 100644 index eea0febfd..000000000 --- a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>DirecTV HD-DVR</Name> - <Identification> - <FriendlyName>^DIRECTV.*$</FriendlyName> - <Headers> - <HttpHeaderInfo name="User-Agent" value="DIRECTV" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>10</TimelineOffsetSeconds> - <RequiresPlainVideoItems>true</RequiresPlainVideoItems> - <RequiresPlainFolders>true</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg2video" type="Video" /> - <DirectPlayProfile container="jpeg,jpg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mpeg" type="Video" videoCodec="mpeg2video" audioCodec="mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles> - <CodecProfile type="Video" codec="mpeg2video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Audio" codec="mp2"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml b/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml deleted file mode 100644 index 5b299577e..000000000 --- a/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml +++ /dev/null @@ -1,96 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Dish Hopper-Joey</Name> - <Identification> - <Manufacturer>Echostar Technologies LLC</Manufacturer> - <ManufacturerUrl>http://www.echostar.com</ManufacturerUrl> - <Headers> - <HttpHeaderInfo name="User-Agent" value="Zip_" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mp2t:http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="mp4,mkv,mpeg,ts" audioCodec="mp3,ac3,aac,he-aac,pcm" videoCodec="h264,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp3,alac,flac" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3,he-aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio"> - <Conditions> - <ProfileCondition condition="Equals" property="IsSecondaryAudio" value="false" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="mkv,ts,mpegts" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml deleted file mode 100644 index 20f5ba79b..000000000 --- a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml +++ /dev/null @@ -1,92 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>LG Smart TV</Name> - <Identification> - <FriendlyName>LG.*</FriendlyName> - <Headers> - <HttpHeaderInfo name="User-Agent" value="LG" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>10</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts,avi,mkv,m2ts" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="mp3" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="mpeg4"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3,eac3,aac,mp3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" type="Video" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="srt" method="External" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml deleted file mode 100644 index d01e3a145..000000000 --- a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml +++ /dev/null @@ -1,54 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Linksys DMA2100</Name> - <Identification> - <ModelName>DMA2100us</ModelName> - <Headers /> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" /> - <DirectPlayProfile container="avi,mp4,mkv,ts,mpegts,m4v" type="Video" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles /> - <ResponseProfiles> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Marantz.xml b/Emby.Dlna/Profiles/Xml/Marantz.xml deleted file mode 100644 index 0cc9c86e8..000000000 --- a/Emby.Dlna/Profiles/Xml/Marantz.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Marantz</Name> - <Identification> - <Manufacturer>Marantz</Manufacturer> - <Headers> - <HttpHeaderInfo name="User-Agent" value="Marantz" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="aac,mp3,wav,wma,flac" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles /> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="External" /> - <SubtitleProfile format="sub" method="External" /> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="ass" method="Embed" /> - <SubtitleProfile format="ssa" method="Embed" /> - <SubtitleProfile format="smi" method="Embed" /> - <SubtitleProfile format="dvdsub" method="Embed" /> - <SubtitleProfile format="pgs" method="Embed" /> - <SubtitleProfile format="pgssub" method="Embed" /> - <SubtitleProfile format="sub" method="Embed" /> - <SubtitleProfile format="subrip" method="Embed" /> - <SubtitleProfile format="vtt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml deleted file mode 100644 index 9d5ddc3d1..000000000 --- a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>MediaMonkey</Name> - <Identification> - <FriendlyName>MediaMonkey</FriendlyName> - <Headers> - <HttpHeaderInfo name="User-Agent" value="MediaMonkey" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="aac,mp3,mpa,wav,wma,mp2,ogg,oga,webma,ape,opus,flac,m4a" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles /> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="External" /> - <SubtitleProfile format="sub" method="External" /> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="ass" method="Embed" /> - <SubtitleProfile format="ssa" method="Embed" /> - <SubtitleProfile format="smi" method="Embed" /> - <SubtitleProfile format="dvdsub" method="Embed" /> - <SubtitleProfile format="pgs" method="Embed" /> - <SubtitleProfile format="pgssub" method="Embed" /> - <SubtitleProfile format="sub" method="Embed" /> - <SubtitleProfile format="subrip" method="Embed" /> - <SubtitleProfile format="vtt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml deleted file mode 100644 index 8f766853b..000000000 --- a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml +++ /dev/null @@ -1,87 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Panasonic Viera</Name> - <Identification> - <FriendlyName>VIERA</FriendlyName> - <Manufacturer>Panasonic</Manufacturer> - <Headers> - <HttpHeaderInfo name="User-Agent" value="Panasonic MIL DLNA" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>10</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:pv" value="http://www.pv.com/pvns/" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,pcm_dvd" videoCodec="mpeg2video,mpeg4" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="aac,ac3,dca,mp3,mp2,pcm,dts" videoCodec="h264,mpeg2video" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="aac,mp3,mp2" videoCodec="h264,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,mp3,pcm" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="mov" audioCodec="aac,pcm" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="flv" audioCodec="aac" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - <DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="srt" method="External" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml deleted file mode 100644 index aa881d014..000000000 --- a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml +++ /dev/null @@ -1,92 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Popcorn Hour</Name> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="mp4,mov,m4v" audioCodec="aac" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,eac3,mp3,mp2,pcm" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="asf,wmv" audioCodec="wmav2,wmapro" videoCodec="wmv3,vc1" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="mp3,ac3,eac3,mp2,pcm" videoCodec="mpeg4,msmpeg4" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="aac,mp3,ac3,eac3,mp2,pcm" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="aac,mp3,flac,ogg,wma,wav" type="Audio" /> - <DirectPlayProfile container="jpeg,gif,bmp,png" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="EqualsAny" property="VideoProfile" value="baseline|constrained baseline" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Audio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Audio" codec="mp3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="320000" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml deleted file mode 100644 index 7160a9c2e..000000000 --- a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ /dev/null @@ -1,128 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Samsung Smart TV</Name> - <Identification> - <ModelUrl>samsung.com</ModelUrl> - <Headers> - <HttpHeaderInfo name="User-Agent" value="SEC_" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:sec" value="http://www.sec.co.kr/" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="asf" audioCodec="mp3,ac3,wmav2,wmapro,wmavoice" videoCodec="h264,mpeg4,mjpeg" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="mp3,ac3,dca,dts" videoCodec="h264,mpeg4,mjpeg" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="mp3,ac3,dca,aac,dts" videoCodec="h264,mpeg4,mjpeg4" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="mp3,aac" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="3gp" audioCodec="aac,he-aac" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="mpg,mpeg" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="vro,vob" audioCodec="ac3,mp2,mp3" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="ts" audioCodec="ac3,aac,mp3,eac3" videoCodec="mpeg2video,h264,vc1" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmavoice" videoCodec="wmv2,wmv3" type="Video" /> - <DirectPlayProfile container="mp3,flac" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="mpeg2video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="30720000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="mpeg4"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="37500000" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="wmv2,wmv3,vc1"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="25600000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="wmav2,dca,aac,mp3,dts"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="avi" type="Video" mimeType="video/x-msvideo"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mkv" type="Video" mimeType="video/x-mkv"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="flac" type="Audio" mimeType="audio/x-flac"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="srt" method="External" didlMode="CaptionInfoEx" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml b/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml deleted file mode 100644 index c9b907e58..000000000 --- a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sharp Smart TV</Name> - <Identification> - <Manufacturer>Sharp</Manufacturer> - <Headers> - <HttpHeaderInfo name="User-Agent" value="Sharp" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>true</RequiresPlainVideoItems> - <RequiresPlainFolders>true</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="m4v,mkv,avi,mov,mp4" audioCodec="aac,mp3,ac3,dts,dca" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="asf,wmv" type="Video" /> - <DirectPlayProfile container="mpg,mpeg" audioCodec="mp3,aac" videoCodec="mpeg2video" type="Video" /> - <DirectPlayProfile container="flv" audioCodec="mp3,aac" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="mp3,wav" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3,dts,dca" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles /> - <ResponseProfiles> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="srt" method="External" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml deleted file mode 100644 index 2c5614883..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml +++ /dev/null @@ -1,87 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Blu-ray Player 2013</Name> - <Identification> - <ModelNumber>BDP-2013</ModelNumber> - <Headers> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1100" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3100" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S5100" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6100" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S7100" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" /> - <DirectPlayProfile container="wmv,asf" type="Video" /> - <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" /> - <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml deleted file mode 100644 index 44f9821b3..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml +++ /dev/null @@ -1,87 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Blu-ray Player 2014</Name> - <Identification> - <ModelNumber>BDP-2014</ModelNumber> - <Headers> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1200" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3200" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S5200" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6200" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S7200" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" /> - <DirectPlayProfile container="wmv,asf" type="Video" /> - <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" /> - <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml deleted file mode 100644 index a7d17c1a0..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml +++ /dev/null @@ -1,85 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Blu-ray Player 2015</Name> - <Identification> - <ModelNumber>BDP-2015</ModelNumber> - <Headers> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1500" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3500" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6500" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" /> - <DirectPlayProfile container="wmv,asf" type="Video" /> - <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" /> - <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml deleted file mode 100644 index b42b1e84f..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml +++ /dev/null @@ -1,85 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Blu-ray Player 2016</Name> - <Identification> - <ModelNumber>BDP-2016</ModelNumber> - <Headers> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1700" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3700" match="Substring" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6700" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" /> - <DirectPlayProfile container="wmv,asf" type="Video" /> - <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" /> - <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml deleted file mode 100644 index 46857caf0..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml +++ /dev/null @@ -1,115 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Blu-ray Player</Name> - <Identification> - <FriendlyName>Blu-ray Disc Player</FriendlyName> - <Manufacturer>Sony</Manufacturer> - <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value="(Blu-ray Disc Player|Home Theater System|Home Theatre System|Media Player)" match="Regex" /> - <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="(Blu-ray Disc Player|Home Theater System|Home Theatre System|Media Player)" match="Regex" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="ac3,mp3,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="avi,mp4,m4v" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="mpeg2video" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg4,vc1" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="avi" type="Video" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mkv" type="Video" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" type="Video" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mp4" type="Video" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mpeg" type="Video" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mp3" type="Audio" mimeType="audio/mpeg"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml deleted file mode 100644 index 1461db311..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml +++ /dev/null @@ -1,133 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Bravia (2010)</Name> - <Identification> - <FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName> - <Manufacturer>Sony</Manufacturer> - <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>http://www.microsoft.com/</ModelUrl> - <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_TN</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="mpeg2video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="mp3,mp2"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" /> - <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml deleted file mode 100644 index 7c5f2b181..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml +++ /dev/null @@ -1,139 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Bravia (2011)</Name> - <Identification> - <FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName> - <Manufacturer>Sony</Manufacturer> - <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>http://www.microsoft.com/</ModelUrl> - <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_TN</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="mp3" videoCodec="mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="mp3" videoCodec="mpeg2video,mpeg1video" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="mpeg2video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="mp3,mp2"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" /> - <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml deleted file mode 100644 index 842a8fba3..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml +++ /dev/null @@ -1,115 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Bravia (2012)</Name> - <Identification> - <FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName> - <Manufacturer>Sony</Manufacturer> - <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>http://www.microsoft.com/</ModelUrl> - <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_TN</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="mp3,mp2"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" /> - <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml deleted file mode 100644 index f1135c3fe..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml +++ /dev/null @@ -1,114 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Bravia (2013)</Name> - <Identification> - <FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName> - <Manufacturer>Sony</Manufacturer> - <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>http://www.microsoft.com/</ModelUrl> - <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_TN</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,eac3,aac,mp3" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="mov" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4,mjpeg" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="ac3,eac3,aac,mp3,mp2,pcm,vorbis" videoCodec="h264,mpeg4,vp8" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,eac3,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mjpeg" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - <DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" /> - <DirectPlayProfile container="wav" audioCodec="pcm" type="Audio" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="mp3,mp2"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" /> - <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml deleted file mode 100644 index 85c7868c6..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml +++ /dev/null @@ -1,114 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony Bravia (2014)</Name> - <Identification> - <FriendlyName>(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*</FriendlyName> - <Manufacturer>Sony</Manufacturer> - <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*" match="Regex" /> - </Headers> - </Identification> - <Manufacturer>Microsoft Corporation</Manufacturer> - <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl> - <ModelName>Windows Media Player Sharing</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>3.0</ModelNumber> - <ModelUrl>http://www.microsoft.com/</ModelUrl> - <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_TN</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes> - <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" /> - </XmlRootAttributes> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,eac3,aac,mp3" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="mov" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4,mjpeg" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="ac3,eac3,aac,mp3,mp2,pcm,vorbis" videoCodec="h264,mpeg4,vp8" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,eac3,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mjpeg" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - <DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" /> - <DirectPlayProfile container="wav" audioCodec="pcm" type="Audio" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="mp3,mp2"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" /> - <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg"> - <Conditions> - <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" /> - </Conditions> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml deleted file mode 100644 index 129b188e2..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ /dev/null @@ -1,105 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony PlayStation 3</Name> - <Identification> - <FriendlyName>PLAYSTATION 3</FriendlyName> - <Headers> - <HttpHeaderInfo name="User-Agent" value="PLAYSTATION 3" match="Substring" /> - <HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 3" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_TN</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="aac,mp3,wav" type="Audio" /> - <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="640000" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="wmapro"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="mp4,mov" audioCodec="aac" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="avi" type="Video" orgPn="AVI" mimeType="video/divx"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="wav" type="Audio" mimeType="audio/wav"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml deleted file mode 100644 index 592119305..000000000 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml +++ /dev/null @@ -1,108 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Sony PlayStation 4</Name> - <Identification> - <FriendlyName>PLAYSTATION 4</FriendlyName> - <Headers> - <HttpHeaderInfo name="User-Agent" value="PLAYSTATION 4" match="Substring" /> - <HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 4" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_TN</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="aac,mp3,wav" type="Audio" /> - <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="640000" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="wmapro"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="mp4,mov" audioCodec="aac" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="avi" type="Video" orgPn="AVI" mimeType="video/divx"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="wav" type="Audio" mimeType="audio/wav"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/WDTV Live.xml b/Emby.Dlna/Profiles/Xml/WDTV Live.xml deleted file mode 100644 index ccb74ee64..000000000 --- a/Emby.Dlna/Profiles/Xml/WDTV Live.xml +++ /dev/null @@ -1,94 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>WDTV Live</Name> - <Identification> - <ModelName>WD TV</ModelName> - <Headers> - <HttpHeaderInfo name="User-Agent" value="alphanetworks" match="Substring" /> - <HttpHeaderInfo name="User-Agent" value="ALPHA Networks" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>5</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>true</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="avi" audioCodec="ac3,eac3,dca,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" /> - <DirectPlayProfile container="mpeg" audioCodec="ac3,eac3,dca,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video" type="Video" /> - <DirectPlayProfile container="mkv" audioCodec="ac3,eac3,dca,aac,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" /> - <DirectPlayProfile container="ts,m2ts,mpegts" audioCodec="ac3,eac3,dca,mp2,mp3,aac,dts" videoCodec="mpeg1video,mpeg2video,h264,vc1" type="Video" /> - <DirectPlayProfile container="mp4,mov,m4v" audioCodec="ac3,eac3,aac,mp2,mp3,dca,dts" videoCodec="h264,mpeg4" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro" videoCodec="vc1" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="mp2,ac3" videoCodec="mpeg2video" type="Video" /> - <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" /> - <DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" /> - <DirectPlayProfile container="flac" type="Audio" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" /> - <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Photo"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="ts,mpegts" type="Video" orgPn="MPEG_TS_SD_NA"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="External" /> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="sub" method="Embed" /> - <SubtitleProfile format="subrip" method="Embed" /> - <SubtitleProfile format="idx" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/Xbox One.xml b/Emby.Dlna/Profiles/Xml/Xbox One.xml deleted file mode 100644 index 26a65bbd4..000000000 --- a/Emby.Dlna/Profiles/Xml/Xbox One.xml +++ /dev/null @@ -1,126 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>Xbox One</Name> - <Identification> - <ModelName>Xbox One</ModelName> - <Headers> - <HttpHeaderInfo name="FriendlyName.DLNA.ORG" value="XboxOne" match="Substring" /> - <HttpHeaderInfo name="User-Agent" value="NSPlayer/12" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>40</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg2video,hevc" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="avi" audioCodec="aac" videoCodec="h264" type="Video" /> - <DirectPlayProfile container="mp4,mov,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4,mpeg2video,hevc" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro" videoCodec="wmv2,wmv3,vc1" type="Video" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> - <DirectPlayProfile container="jpeg" type="Photo" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" videoCodec="jpeg" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles> - <ContainerProfile type="Video" container="mp4,mov"> - <Conditions> - <ProfileCondition condition="Equals" property="Has64BitOffsets" value="false" isRequired="false" /> - </Conditions> - </ContainerProfile> - </ContainerProfiles> - <CodecProfiles> - <CodecProfile type="Video" codec="mpeg4"> - <Conditions> - <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="5120000" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="h264"> - <Conditions> - <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" /> - <ProfileCondition condition="EqualsAny" property="VideoProfile" value="high|main|baseline|constrained baseline" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video" codec="wmv2,wmv3,vc1"> - <Conditions> - <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" /> - <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="Video"> - <Conditions> - <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" /> - <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="ac3,wmav2,wmapro"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - <CodecProfile type="VideoAudio" codec="aac"> - <Conditions> - <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" /> - <ProfileCondition condition="Equals" property="AudioProfile" value="lc" isRequired="false" /> - </Conditions> - <ApplyConditions /> - </CodecProfile> - </CodecProfiles> - <ResponseProfiles> - <ResponseProfile container="avi" type="Video" mimeType="video/avi"> - <Conditions /> - </ResponseProfile> - <ResponseProfile container="m4v" type="Video" mimeType="video/mp4"> - <Conditions /> - </ResponseProfile> - </ResponseProfiles> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Profiles/Xml/foobar2000.xml b/Emby.Dlna/Profiles/Xml/foobar2000.xml deleted file mode 100644 index 5ce75ace5..000000000 --- a/Emby.Dlna/Profiles/Xml/foobar2000.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0"?> -<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <Name>foobar2000</Name> - <Identification> - <FriendlyName>foobar</FriendlyName> - <Headers> - <HttpHeaderInfo name="User-Agent" value="foobar" match="Substring" /> - </Headers> - </Identification> - <Manufacturer>Jellyfin</Manufacturer> - <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl> - <ModelName>Jellyfin Server</ModelName> - <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription> - <ModelNumber>01</ModelNumber> - <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl> - <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl> - <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit> - <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit> - <SupportedMediaTypes>Audio</SupportedMediaTypes> - <AlbumArtPn>JPEG_SM</AlbumArtPn> - <MaxAlbumArtWidth>480</MaxAlbumArtWidth> - <MaxAlbumArtHeight>480</MaxAlbumArtHeight> - <MaxIconWidth>48</MaxIconWidth> - <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>140000000</MaxStreamingBitrate> - <MaxStaticBitrate>140000000</MaxStaticBitrate> - <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> - <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> - <TimelineOffsetSeconds>0</TimelineOffsetSeconds> - <RequiresPlainVideoItems>false</RequiresPlainVideoItems> - <RequiresPlainFolders>false</RequiresPlainFolders> - <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar> - <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests> - <XmlRootAttributes /> - <DirectPlayProfiles> - <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" /> - <DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" /> - <DirectPlayProfile container="aac,wav" type="Audio" /> - <DirectPlayProfile container="flac" audioCodec="flac" type="Audio" /> - <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> - <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" /> - </DirectPlayProfiles> - <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - </TranscodingProfiles> - <ContainerProfiles /> - <CodecProfiles /> - <ResponseProfiles /> - <SubtitleProfiles> - <SubtitleProfile format="srt" method="External" /> - <SubtitleProfile format="sub" method="External" /> - <SubtitleProfile format="srt" method="Embed" /> - <SubtitleProfile format="ass" method="Embed" /> - <SubtitleProfile format="ssa" method="Embed" /> - <SubtitleProfile format="smi" method="Embed" /> - <SubtitleProfile format="dvdsub" method="Embed" /> - <SubtitleProfile format="pgs" method="Embed" /> - <SubtitleProfile format="pgssub" method="Embed" /> - <SubtitleProfile format="sub" method="Embed" /> - <SubtitleProfile format="subrip" method="Embed" /> - <SubtitleProfile format="vtt" method="Embed" /> - </SubtitleProfiles> -</Profile> diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs deleted file mode 100644 index 606ffcf4f..000000000 --- a/Emby.Dlna/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Emby.Dlna")] -[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")] -[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs deleted file mode 100644 index 69ef6f645..000000000 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ /dev/null @@ -1,358 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Security; -using System.Text; -using Emby.Dlna.Common; -using MediaBrowser.Model.Dlna; - -namespace Emby.Dlna.Server -{ - public class DescriptionXmlBuilder - { - private readonly DeviceProfile _profile; - - private readonly string _serverUdn; - private readonly string _serverAddress; - private readonly string _serverName; - private readonly string _serverId; - - public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId) - { - ArgumentException.ThrowIfNullOrEmpty(serverUdn); - ArgumentException.ThrowIfNullOrEmpty(serverAddress); - - _profile = profile; - _serverUdn = serverUdn; - _serverAddress = serverAddress; - _serverName = serverName; - _serverId = serverId; - } - - public string GetXml() - { - var builder = new StringBuilder(); - - builder.Append("<?xml version=\"1.0\"?>"); - - builder.Append("<root"); - - var attributes = _profile.XmlRootAttributes.ToList(); - - attributes.Insert(0, new XmlAttribute - { - Name = "xmlns:dlna", - Value = "urn:schemas-dlna-org:device-1-0" - }); - attributes.Insert(0, new XmlAttribute - { - Name = "xmlns", - Value = "urn:schemas-upnp-org:device-1-0" - }); - - foreach (var att in attributes) - { - builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value); - } - - builder.Append('>'); - - builder.Append("<specVersion>"); - builder.Append("<major>1</major>"); - builder.Append("<minor>0</minor>"); - builder.Append("</specVersion>"); - - AppendDeviceInfo(builder); - - builder.Append("</root>"); - - return builder.ToString(); - } - - private void AppendDeviceInfo(StringBuilder builder) - { - builder.Append("<device>"); - AppendDeviceProperties(builder); - - AppendIconList(builder); - - builder.Append("<presentationURL>") - .Append(SecurityElement.Escape(_serverAddress)) - .Append("/web/index.html</presentationURL>"); - - AppendServiceList(builder); - builder.Append("</device>"); - } - - private void AppendDeviceProperties(StringBuilder builder) - { - builder.Append("<dlna:X_DLNACAP/>"); - - builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">DMS-1.50</dlna:X_DLNADOC>"); - builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">M-DMS-1.50</dlna:X_DLNADOC>"); - - builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"); - - builder.Append("<friendlyName>") - .Append(SecurityElement.Escape(GetFriendlyName())) - .Append("</friendlyName>"); - builder.Append("<manufacturer>") - .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty)) - .Append("</manufacturer>"); - builder.Append("<manufacturerURL>") - .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty)) - .Append("</manufacturerURL>"); - - builder.Append("<modelDescription>") - .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty)) - .Append("</modelDescription>"); - builder.Append("<modelName>") - .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty)) - .Append("</modelName>"); - - builder.Append("<modelNumber>") - .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty)) - .Append("</modelNumber>"); - builder.Append("<modelURL>") - .Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty)) - .Append("</modelURL>"); - - if (string.IsNullOrEmpty(_profile.SerialNumber)) - { - builder.Append("<serialNumber>") - .Append(SecurityElement.Escape(_serverId)) - .Append("</serialNumber>"); - } - else - { - builder.Append("<serialNumber>") - .Append(SecurityElement.Escape(_profile.SerialNumber)) - .Append("</serialNumber>"); - } - - builder.Append("<UPC/>"); - - builder.Append("<UDN>uuid:") - .Append(SecurityElement.Escape(_serverUdn)) - .Append("</UDN>"); - - if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags)) - { - builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">") - .Append(SecurityElement.Escape(_profile.SonyAggregationFlags)) - .Append("</av:aggregationFlags>"); - } - } - - internal string GetFriendlyName() - { - if (string.IsNullOrEmpty(_profile.FriendlyName)) - { - return _serverName; - } - - if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase)) - { - return _profile.FriendlyName; - } - - var characterList = new List<char>(); - - foreach (var c in _serverName) - { - if (char.IsLetterOrDigit(c) || c == '-') - { - characterList.Add(c); - } - } - - var serverName = string.Create( - characterList.Count, - characterList, - (dest, source) => - { - for (int i = 0; i < dest.Length; i++) - { - dest[i] = source[i]; - } - }); - - return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase); - } - - private void AppendIconList(StringBuilder builder) - { - builder.Append("<iconList>"); - - foreach (var icon in GetIcons()) - { - builder.Append("<icon>"); - - builder.Append("<mimetype>") - .Append(SecurityElement.Escape(icon.MimeType)) - .Append("</mimetype>"); - builder.Append("<width>") - .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture))) - .Append("</width>"); - builder.Append("<height>") - .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture))) - .Append("</height>"); - builder.Append("<depth>") - .Append(SecurityElement.Escape(icon.Depth)) - .Append("</depth>"); - builder.Append("<url>") - .Append(BuildUrl(icon.Url)) - .Append("</url>"); - - builder.Append("</icon>"); - } - - builder.Append("</iconList>"); - } - - private void AppendServiceList(StringBuilder builder) - { - builder.Append("<serviceList>"); - - foreach (var service in GetServices()) - { - builder.Append("<service>"); - - builder.Append("<serviceType>") - .Append(SecurityElement.Escape(service.ServiceType)) - .Append("</serviceType>"); - builder.Append("<serviceId>") - .Append(SecurityElement.Escape(service.ServiceId)) - .Append("</serviceId>"); - builder.Append("<SCPDURL>") - .Append(BuildUrl(service.ScpdUrl)) - .Append("</SCPDURL>"); - builder.Append("<controlURL>") - .Append(BuildUrl(service.ControlUrl)) - .Append("</controlURL>"); - builder.Append("<eventSubURL>") - .Append(BuildUrl(service.EventSubUrl)) - .Append("</eventSubURL>"); - - builder.Append("</service>"); - } - - builder.Append("</serviceList>"); - } - - private string BuildUrl(string url) - { - if (string.IsNullOrEmpty(url)) - { - return string.Empty; - } - - url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/'); - - return SecurityElement.Escape(url); - } - - private IEnumerable<DeviceIcon> GetIcons() - => new[] - { - new DeviceIcon - { - MimeType = "image/png", - Depth = "24", - Width = 240, - Height = 240, - Url = "icons/logo240.png" - }, - - new DeviceIcon - { - MimeType = "image/jpeg", - Depth = "24", - Width = 240, - Height = 240, - Url = "icons/logo240.jpg" - }, - - new DeviceIcon - { - MimeType = "image/png", - Depth = "24", - Width = 120, - Height = 120, - Url = "icons/logo120.png" - }, - - new DeviceIcon - { - MimeType = "image/jpeg", - Depth = "24", - Width = 120, - Height = 120, - Url = "icons/logo120.jpg" - }, - - new DeviceIcon - { - MimeType = "image/png", - Depth = "24", - Width = 48, - Height = 48, - Url = "icons/logo48.png" - }, - - new DeviceIcon - { - MimeType = "image/jpeg", - Depth = "24", - Width = 48, - Height = 48, - Url = "icons/logo48.jpg" - } - }; - - private IEnumerable<DeviceService> GetServices() - { - var list = new List<DeviceService>(); - - list.Add(new DeviceService - { - ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1", - ServiceId = "urn:upnp-org:serviceId:ContentDirectory", - ScpdUrl = "contentdirectory/contentdirectory.xml", - ControlUrl = "contentdirectory/control", - EventSubUrl = "contentdirectory/events" - }); - - list.Add(new DeviceService - { - ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1", - ServiceId = "urn:upnp-org:serviceId:ConnectionManager", - ScpdUrl = "connectionmanager/connectionmanager.xml", - ControlUrl = "connectionmanager/control", - EventSubUrl = "connectionmanager/events" - }); - - if (_profile.EnableMSMediaReceiverRegistrar) - { - list.Add(new DeviceService - { - ServiceType = "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", - ServiceId = "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar", - ScpdUrl = "mediareceiverregistrar/mediareceiverregistrar.xml", - ControlUrl = "mediareceiverregistrar/control", - EventSubUrl = "mediareceiverregistrar/events" - }); - } - - return list; - } - - public override string ToString() - { - return GetXml(); - } - } -} diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs deleted file mode 100644 index bff5307a4..000000000 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ /dev/null @@ -1,242 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using Emby.Dlna.Didl; -using Jellyfin.Extensions; -using MediaBrowser.Controller.Configuration; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.Service -{ - public abstract class BaseControlHandler - { - private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/"; - - protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) - { - Config = config; - Logger = logger; - } - - protected IServerConfigurationManager Config { get; } - - protected ILogger Logger { get; } - - public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) - { - try - { - LogRequest(request); - - var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false); - LogResponse(response); - return response; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error processing control request"); - - return ControlErrorHandler.GetResponse(ex); - } - } - - private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request) - { - ControlRequestInfo requestInfo; - - using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8)) - { - var readerSettings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - Async = true - }; - - using var reader = XmlReader.Create(streamReader, readerSettings); - requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); - } - - Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers); - - return CreateControlResponse(requestInfo); - } - - private ControlResponse CreateControlResponse(ControlRequestInfo requestInfo) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false - }; - - StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); - - using (var writer = XmlWriter.Create(builder, settings)) - { - writer.WriteStartDocument(true); - - writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv); - writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/"); - - writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv); - writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI); - - WriteResult(requestInfo.LocalName, requestInfo.Headers, writer); - - writer.WriteFullEndElement(); - writer.WriteFullEndElement(); - - writer.WriteFullEndElement(); - writer.WriteEndDocument(); - } - - var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal); - - var controlResponse = new ControlResponse(xml, true); - - controlResponse.Headers.Add("EXT", string.Empty); - - return controlResponse; - } - - private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader) - { - await reader.MoveToContentAsync().ConfigureAwait(false); - await reader.ReadAsync().ConfigureAwait(false); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - if (string.Equals(reader.LocalName, "Body", StringComparison.Ordinal)) - { - if (reader.IsEmptyElement) - { - await reader.ReadAsync().ConfigureAwait(false); - continue; - } - - using var subReader = reader.ReadSubtree(); - return await ParseBodyTagAsync(subReader).ConfigureAwait(false); - } - - await reader.SkipAsync().ConfigureAwait(false); - } - else - { - await reader.ReadAsync().ConfigureAwait(false); - } - } - - throw new EndOfStreamException("Stream ended but no body tag found."); - } - - private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader) - { - string? namespaceURI = null, localName = null; - - await reader.MoveToContentAsync().ConfigureAwait(false); - await reader.ReadAsync().ConfigureAwait(false); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - localName = reader.LocalName; - namespaceURI = reader.NamespaceURI; - - if (reader.IsEmptyElement) - { - await reader.ReadAsync().ConfigureAwait(false); - } - else - { - var result = new ControlRequestInfo(localName, namespaceURI); - using var subReader = reader.ReadSubtree(); - await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); - return result; - } - } - else - { - await reader.ReadAsync().ConfigureAwait(false); - } - } - - if (localName is not null && namespaceURI is not null) - { - return new ControlRequestInfo(localName, namespaceURI); - } - - throw new EndOfStreamException("Stream ended but no control found."); - } - - private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers) - { - await reader.MoveToContentAsync().ConfigureAwait(false); - await reader.ReadAsync().ConfigureAwait(false); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - // TODO: Should we be doing this here, or should it be handled earlier when decoding the request? - headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false); - } - else - { - await reader.ReadAsync().ConfigureAwait(false); - } - } - } - - protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter); - - private void LogRequest(ControlRequest request) - { - if (!Config.GetDlnaConfiguration().EnableDebugLog) - { - return; - } - - Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers); - } - - private void LogResponse(ControlResponse response) - { - if (!Config.GetDlnaConfiguration().EnableDebugLog) - { - return; - } - - Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml); - } - - private class ControlRequestInfo - { - public ControlRequestInfo(string localName, string namespaceUri) - { - LocalName = localName; - NamespaceURI = namespaceUri; - Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - public string LocalName { get; set; } - - public string NamespaceURI { get; set; } - - public Dictionary<string, string> Headers { get; } - } - } -} diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs deleted file mode 100644 index 67e7bf6a6..000000000 --- a/Emby.Dlna/Service/BaseService.cs +++ /dev/null @@ -1,37 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System.Net.Http; -using Emby.Dlna.Eventing; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.Service -{ - public class BaseService : IDlnaEventManager - { - protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory) - { - Logger = logger; - EventManager = new DlnaEventManager(logger, httpClientFactory); - } - - protected IDlnaEventManager EventManager { get; } - - protected ILogger Logger { get; } - - public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) - { - return EventManager.CancelEventSubscription(subscriptionId); - } - - public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl) - { - return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl); - } - - public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) - { - return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl); - } - } -} diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs deleted file mode 100644 index 3e2cd6d2e..000000000 --- a/Emby.Dlna/Service/ControlErrorHandler.cs +++ /dev/null @@ -1,52 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Text; -using System.Xml; -using Emby.Dlna.Didl; - -namespace Emby.Dlna.Service -{ - public static class ControlErrorHandler - { - private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/"; - - public static ControlResponse GetResponse(Exception ex) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false - }; - - StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); - - using (var writer = XmlWriter.Create(builder, settings)) - { - writer.WriteStartDocument(true); - - writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv); - writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/"); - - writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv); - writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv); - - writer.WriteElementString("faultcode", "500"); - writer.WriteElementString("faultstring", ex.Message); - - writer.WriteStartElement("detail"); - writer.WriteRaw("<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\"><errorCode>401</errorCode><errorDescription>Invalid Action</errorDescription></UPnPError>"); - writer.WriteFullEndElement(); - - writer.WriteFullEndElement(); - writer.WriteFullEndElement(); - - writer.WriteFullEndElement(); - writer.WriteEndDocument(); - } - - return new ControlResponse(builder.ToString(), false); - } - } -} diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs deleted file mode 100644 index 6e0bc6ad8..000000000 --- a/Emby.Dlna/Service/ServiceXmlBuilder.cs +++ /dev/null @@ -1,109 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Security; -using System.Text; -using Emby.Dlna.Common; - -namespace Emby.Dlna.Service -{ - public class ServiceXmlBuilder - { - public string GetXml(IEnumerable<ServiceAction> actions, IEnumerable<StateVariable> stateVariables) - { - var builder = new StringBuilder(); - - builder.Append("<?xml version=\"1.0\"?>"); - builder.Append("<scpd xmlns=\"urn:schemas-upnp-org:service-1-0\">"); - - builder.Append("<specVersion>"); - builder.Append("<major>1</major>"); - builder.Append("<minor>0</minor>"); - builder.Append("</specVersion>"); - - AppendActionList(builder, actions); - AppendServiceStateTable(builder, stateVariables); - - builder.Append("</scpd>"); - - return builder.ToString(); - } - - private static void AppendActionList(StringBuilder builder, IEnumerable<ServiceAction> actions) - { - builder.Append("<actionList>"); - - foreach (var item in actions) - { - builder.Append("<action>"); - - builder.Append("<name>") - .Append(SecurityElement.Escape(item.Name)) - .Append("</name>"); - - builder.Append("<argumentList>"); - - foreach (var argument in item.ArgumentList) - { - builder.Append("<argument>"); - - builder.Append("<name>") - .Append(SecurityElement.Escape(argument.Name)) - .Append("</name>"); - builder.Append("<direction>") - .Append(SecurityElement.Escape(argument.Direction)) - .Append("</direction>"); - builder.Append("<relatedStateVariable>") - .Append(SecurityElement.Escape(argument.RelatedStateVariable)) - .Append("</relatedStateVariable>"); - - builder.Append("</argument>"); - } - - builder.Append("</argumentList>"); - - builder.Append("</action>"); - } - - builder.Append("</actionList>"); - } - - private static void AppendServiceStateTable(StringBuilder builder, IEnumerable<StateVariable> stateVariables) - { - builder.Append("<serviceStateTable>"); - - foreach (var item in stateVariables) - { - var sendEvents = item.SendsEvents ? "yes" : "no"; - - builder.Append("<stateVariable sendEvents=\"") - .Append(sendEvents) - .Append("\">"); - - builder.Append("<name>") - .Append(SecurityElement.Escape(item.Name)) - .Append("</name>"); - builder.Append("<dataType>") - .Append(SecurityElement.Escape(item.DataType)) - .Append("</dataType>"); - - if (item.AllowedValues.Count > 0) - { - builder.Append("<allowedValueList>"); - foreach (var allowedValue in item.AllowedValues) - { - builder.Append("<allowedValue>") - .Append(SecurityElement.Escape(allowedValue)) - .Append("</allowedValue>"); - } - - builder.Append("</allowedValueList>"); - } - - builder.Append("</stateVariable>"); - } - - builder.Append("</serviceStateTable>"); - } - } -} diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs deleted file mode 100644 index 4fbbc3885..000000000 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ /dev/null @@ -1,151 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using Jellyfin.Data.Events; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Dlna; -using Rssdp; -using Rssdp.Infrastructure; - -namespace Emby.Dlna.Ssdp -{ - public sealed class DeviceDiscovery : IDeviceDiscovery, IDisposable - { - private readonly object _syncLock = new object(); - - private readonly IServerConfigurationManager _config; - - private SsdpDeviceLocator _deviceLocator; - private ISsdpCommunicationsServer _commsServer; - - private int _listenerCount; - private bool _disposed; - - public DeviceDiscovery(IServerConfigurationManager config) - { - _config = config; - } - - private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal; - - /// <inheritdoc /> - public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered - { - add - { - lock (_syncLock) - { - _listenerCount++; - DeviceDiscoveredInternal += value; - } - - StartInternal(); - } - - remove - { - lock (_syncLock) - { - _listenerCount--; - DeviceDiscoveredInternal -= value; - } - } - } - - /// <inheritdoc /> - public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft; - - // Call this method from somewhere in your code to start the search. - public void Start(ISsdpCommunicationsServer communicationsServer) - { - _commsServer = communicationsServer; - - StartInternal(); - } - - private void StartInternal() - { - lock (_syncLock) - { - if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null) - { - _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 - // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method). - // _DeviceLocator.NotificationFilter = "upnp:rootdevice"; - - // Connect our event handler so we process devices as they are found - _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable; - _deviceLocator.DeviceUnavailable += OnDeviceLocatorDeviceUnavailable; - - var dueTime = TimeSpan.FromSeconds(5); - var interval = TimeSpan.FromSeconds(_config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds); - - _deviceLocator.RestartBroadcastTimer(dueTime, interval); - } - } - } - - // Process each found device in the event handler - private void OnDeviceLocatorDeviceAvailable(object sender, DeviceAvailableEventArgs e) - { - var originalHeaders = e.DiscoveredDevice.ResponseHeaders; - - var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase); - - var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - - var args = new GenericEventArgs<UpnpDeviceInfo>( - new UpnpDeviceInfo - { - Location = e.DiscoveredDevice.DescriptionLocation, - Headers = headers, - RemoteIPAddress = e.RemoteIPAddress - }); - - DeviceDiscoveredInternal?.Invoke(this, args); - } - - private void OnDeviceLocatorDeviceUnavailable(object sender, DeviceUnavailableEventArgs e) - { - var originalHeaders = e.DiscoveredDevice.ResponseHeaders; - - var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase); - - var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - - var args = new GenericEventArgs<UpnpDeviceInfo>( - new UpnpDeviceInfo - { - Location = e.DiscoveredDevice.DescriptionLocation, - Headers = headers - }); - - DeviceLeft?.Invoke(this, args); - } - - /// <inheritdoc /> - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - if (_deviceLocator is not null) - { - _deviceLocator.Dispose(); - _deviceLocator = null; - } - } - } - } -} diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs deleted file mode 100644 index d00eb02b4..000000000 --- a/Emby.Dlna/Ssdp/SsdpExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System.Linq; -using System.Xml.Linq; - -namespace Emby.Dlna.Ssdp -{ - public static class SsdpExtensions - { - public static string? GetValue(this XElement container, XName name) - { - var node = container.Element(name); - - return node?.Value; - } - - public static string? GetAttributeValue(this XElement container, XName name) - { - var node = container.Attribute(name); - - return node?.Value; - } - - public static string? GetDescendantValue(this XElement container, XName name) - => container.Descendants(name).FirstOrDefault()?.Value; - } -} diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 4540ab205..dce56e0a4 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -13,7 +13,6 @@ using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using Emby.Dlna.Main; using Emby.Naming.Common; using Emby.Photos; using Emby.Server.Implementations.Channels; @@ -28,7 +27,6 @@ using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; -using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Plugins; using Emby.Server.Implementations.QuickConnect; @@ -42,6 +40,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Manager; +using Jellyfin.Networking.Udp; using Jellyfin.Server.Implementations; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -867,9 +866,6 @@ namespace Emby.Server.Implementations // MediaEncoding yield return typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder).Assembly; - // Dlna - yield return typeof(DlnaHost).Assembly; - // Local metadata yield return typeof(BoxSetXmlSaver).Assembly; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 905f36e43..b3344bb9f 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -14,7 +14,6 @@ <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> - <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" /> <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" /> @@ -30,7 +29,6 @@ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> - <PackageReference Include="Mono.Nat" /> <PackageReference Include="prometheus-net.DotNetRuntime" /> <PackageReference Include="DotNet.Glob" /> </ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs deleted file mode 100644 index c4cd935c3..000000000 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ /dev/null @@ -1,208 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Logging; -using Mono.Nat; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// <summary> - /// Server entrypoint handling external port forwarding. - /// </summary> - public class ExternalPortForwarding : IServerEntryPoint - { - private readonly IServerApplicationHost _appHost; - private readonly ILogger<ExternalPortForwarding> _logger; - private readonly IServerConfigurationManager _config; - - private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>(); - - private Timer _timer; - private string _configIdentifier; - - private bool _disposed = false; - - /// <summary> - /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="appHost">The application host.</param> - /// <param name="config">The configuration manager.</param> - public ExternalPortForwarding( - ILogger<ExternalPortForwarding> logger, - IServerApplicationHost appHost, - IServerConfigurationManager config) - { - _logger = logger; - _appHost = appHost; - _config = config; - } - - private string GetConfigIdentifier() - { - const char Separator = '|'; - var config = _config.GetNetworkConfiguration(); - - return new StringBuilder(32) - .Append(config.EnableUPnP).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) - .Append(config.EnableRemoteAccess).Append(Separator) - .ToString(); - } - - private void OnConfigurationUpdated(object sender, EventArgs e) - { - var oldConfigIdentifier = _configIdentifier; - _configIdentifier = GetConfigIdentifier(); - - if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase)) - { - Stop(); - Start(); - } - } - - /// <inheritdoc /> - public Task RunAsync() - { - Start(); - - _config.ConfigurationUpdated += OnConfigurationUpdated; - - return Task.CompletedTask; - } - - private void Start() - { - var config = _config.GetNetworkConfiguration(); - if (!config.EnableUPnP || !config.EnableRemoteAccess) - { - return; - } - - _logger.LogInformation("Starting NAT discovery"); - - NatUtility.DeviceFound += OnNatUtilityDeviceFound; - NatUtility.StartDiscovery(); - - _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); - } - - private void Stop() - { - _logger.LogInformation("Stopping NAT discovery"); - - NatUtility.StopDiscovery(); - NatUtility.DeviceFound -= OnNatUtilityDeviceFound; - - _timer?.Dispose(); - } - - private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) - { - try - { - await CreateRules(e.Device).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating port forwarding rules"); - } - } - - private Task CreateRules(INatDevice device) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - // On some systems the device discovered event seems to fire repeatedly - // This check will help ensure we're not trying to port map the same device over and over - if (!_createdRules.TryAdd(device.DeviceEndpoint, 0)) - { - return Task.CompletedTask; - } - - return Task.WhenAll(CreatePortMaps(device)); - } - - private IEnumerable<Task> CreatePortMaps(INatDevice device) - { - var config = _config.GetNetworkConfiguration(); - yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort); - - if (_appHost.ListenWithHttps) - { - yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); - } - } - - private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort) - { - _logger.LogDebug( - "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}", - privatePort, - publicPort, - device.DeviceEndpoint); - - try - { - var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name); - await device.CreatePortMapAsync(mapping).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.", - privatePort, - publicPort, - device.DeviceEndpoint); - } - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <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 (_disposed) - { - return; - } - - _config.ConfigurationUpdated -= OnConfigurationUpdated; - - Stop(); - - _timer = null; - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs deleted file mode 100644 index 56ccb21ee..000000000 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Udp; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -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 -{ - /// <summary> - /// Class responsible for registering all UDP broadcast endpoints and their handlers. - /// </summary> - public sealed class UdpServerEntryPoint : IServerEntryPoint - { - /// <summary> - /// The port of the UDP server. - /// </summary> - public const int PortNumber = 7359; - - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<UdpServerEntryPoint> _logger; - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _config; - private readonly IConfigurationManager _configurationManager; - private readonly INetworkManager _networkManager; - - /// <summary> - /// The UDP server. - /// </summary> - 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. - /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param> - /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> - /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> - /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> - public UdpServerEntryPoint( - ILogger<UdpServerEntryPoint> logger, - IServerApplicationHost appHost, - IConfiguration configuration, - IConfigurationManager configurationManager, - INetworkManager networkManager) - { - _logger = logger; - _appHost = appHost; - _config = configuration; - _configurationManager = configurationManager; - _networkManager = networkManager; - _udpServers = new List<UdpServer>(); - } - - /// <inheritdoc /> - public Task RunAsync() - { - CheckDisposed(); - - if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery) - { - return Task.CompletedTask; - } - - try - { - // 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 listener - var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, 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 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); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); - } - } - else - { - // 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) - { - _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber); - } - - return Task.CompletedTask; - } - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - foreach (var server in _udpServers) - { - server.Dispose(); - } - - _udpServers.Clear(); - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index ecdc01a3d..35387d032 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -125,5 +125,6 @@ "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "External": "خارجي", "HearingImpaired": "ضعاف السمع", - "TaskRefreshTrickplayImages": "توليد صور Trickplay" + "TaskRefreshTrickplayImages": "توليد صور Trickplay", + "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة." } diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index e1cf1448b..3810e8b34 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -125,5 +125,6 @@ "TaskKeyframeExtractor": "Извличане на ключови кадри", "External": "Външен", "HearingImpaired": "Увреден слух", - "TaskRefreshTrickplayImages": "Генерирай изображение" + "TaskRefreshTrickplayImages": "Генерирай изображение", + "TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки." } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 005926231..4724bba3b 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -123,5 +123,7 @@ "External": "বাহ্যিক", "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস", "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক", - "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।" + "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।", + "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন", + "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 837172a5b..092af34b6 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -124,5 +124,7 @@ "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" + "HearingImpaired": "Hørehæmmet", + "TaskRefreshTrickplayImages": "Generér Trickplay Billeder", + "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker." } diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json index e91084f92..114c76c54 100644 --- a/Emby.Server.Implementations/Localization/Core/eu.json +++ b/Emby.Server.Implementations/Localization/Core/eu.json @@ -123,5 +123,7 @@ "HeaderRecordingGroups": "Grabaketa taldeak", "Inherit": "Oinordetu", "TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.", - "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua" + "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua", + "TaskRefreshTrickplayImages": "\"Trickplay Irudiak Sortu", + "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan." } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 88a4a358e..55ee1abaa 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -124,5 +124,6 @@ "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", - "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe" + "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe", + "TaskRefreshTrickplayImagesDescription": "Nanggagawa ng mga trickplay prebiyu para sa mga bidyo sa pinaganang mga aklatan." } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 03002476c..e0aff7954 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -105,7 +105,7 @@ "TaskRefreshPeople": "Actualiser les acteurs", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogs": "Nettoyer le répertoire des journaux", - "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.", + "TaskRefreshLibraryDescription": "Analyse votre médiathèque pour trouver de nouveaux fichiers et actualise 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", diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index ac9da1dd1..b95d07d5c 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -124,5 +124,7 @@ "TaskOptimizeDatabaseDescription": "Kompromiert d Datenbank und trennt freie Speicherplatz. Durch die Ufagb cha d Leistig nach em ne Scan vor Bibliothek oder andere Ufgabe verbesseret werde.", "HearingImpaired": "Hörgschädigti", "TaskKeyframeExtractor": "Keyframe-Extraktor", - "TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe." + "TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe.", + "TaskRefreshTrickplayImages": "Trickplay-Bilder erstelle", + "TaskRefreshTrickplayImagesDescription": "Erstellt Trickplay-Vorschaue für Video in aktivierte Bibliothèke." } diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 47d3eeac5..3f4dea523 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": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे." } diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 87ce07da3..78a443348 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." } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 5c7dec7ef..0362c2417 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.", "TaskKeyframeExtractor": "Nøkkelbilde-uttrekker", "External": "Ekstern", - "HearingImpaired": "Hørselshemmet" + "HearingImpaired": "Hørselshemmet", + "TaskRefreshTrickplayImages": "Generer Trickplay bilder", + "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker." } diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index d1b73a3eb..91ed11042 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Nxjerrë kornizat kryesore nga skedarët video për të krijuar lista luajtjeje më të sakta HLS. Ky veprim mund të dojë një kohë të gjatë për tu kompletuar.", "TaskKeyframeExtractor": "Nxjerrës i kornizës kryesore", "External": "Jashtem", - "HearingImpaired": "Dëgjimi i dëmtuar" + "HearingImpaired": "Dëgjimi i dëmtuar", + "TaskRefreshTrickplayImages": "Krijo Imazhe Trickplay", + "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara." } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 3ce928859..a4877f4b5 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -23,9 +23,9 @@ "HeaderFavoriteShows": "Favori Diziler", "HeaderFavoriteSongs": "Favori Şarkılar", "HeaderLiveTV": "Canlı TV", - "HeaderNextUp": "Gelecek Hafta", + "HeaderNextUp": "Sıradaki Bölümler", "HeaderRecordingGroups": "Kayıt Grupları", - "HomeVideos": "Ana Sayfa Videoları", + "HomeVideos": "Ana Ekran Videoları", "Inherit": "Devral", "ItemAddedWithName": "{0} kütüphaneye eklendi", "ItemRemovedWithName": "{0} kütüphaneden silindi", @@ -81,7 +81,7 @@ "User": "Kullanıcı", "UserCreatedWithName": "{0} kullanıcısı oluşturuldu", "UserDeletedWithName": "{0} kullanıcısı silindi", - "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor", + "UserDownloadingItemWithValues": "{0} kullanıcısı {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", @@ -92,7 +92,7 @@ "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi", "ValueSpecialEpisodeName": "Özel - {0}", "VersionNumber": "Sürüm {0}", - "TaskCleanCache": "Geçici Dosya Klasörünü Temizle", + "TaskCleanCache": "Önbellek Dizinini Temizle", "TasksChannelsCategory": "İnternet Kanalları", "TasksApplicationCategory": "Uygulama", "TasksLibraryCategory": "Kütüphane", @@ -116,13 +116,15 @@ "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.", "TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle", "TaskCleanActivityLogDescription": "Yapılandırılan tarihten daha eski olan etkinlik günlüğü girişlerini siler.", - "Undefined": "Bilinmeyen", + "Undefined": "Tanımlanmadı", "Default": "Varsayılan", "Forced": "Zorla", - "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kitaplığı taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.", + "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kütüphaneyi taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.", "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", + "TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından ana kareleri çıkarır. Bu görev uzun bir süre çalışabilir.", + "TaskKeyframeExtractor": "Ana Kare Çıkarıcı", "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." } diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs deleted file mode 100644 index 2bcd5eab2..000000000 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ /dev/null @@ -1,119 +0,0 @@ -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 /> - public Socket CreateUdpBroadcastSocket(int localPort) - { - if (localPort < 0) - { - throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); - } - - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - try - { - 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 socket; - } - catch - { - socket.Dispose(); - - throw; - } - } - - /// <inheritdoc /> - public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) - { - var interfaceAddress = bindInterface.Address; - ArgumentNullException.ThrowIfNull(interfaceAddress); - - if (localPort < 0) - { - throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); - } - - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - try - { - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(new IPEndPoint(interfaceAddress, localPort)); - - return socket; - } - catch - { - socket.Dispose(); - - throw; - } - } - - /// <inheritdoc /> - public Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort) - { - var bindIPAddress = bindInterface.Address; - ArgumentNullException.ThrowIfNull(multicastAddress); - ArgumentNullException.ThrowIfNull(bindIPAddress); - - if (multicastTimeToLive <= 0) - { - throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive)); - } - - if (localPort < 0) - { - throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); - } - - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - - try - { - socket.MulticastLoopback = false; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - - 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; - } - catch - { - socket.Dispose(); - - throw; - } - } - } -} diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index 2c477218f..c4552474c 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs deleted file mode 100644 index 2d806c146..000000000 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller; -using MediaBrowser.Model.ApiClient; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; - -namespace Emby.Server.Implementations.Udp -{ - /// <summary> - /// Provides a Udp Server. - /// </summary> - public sealed class UdpServer : IDisposable - { - /// <summary> - /// The _logger. - /// </summary> - private readonly ILogger _logger; - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _config; - - private readonly byte[] _receiveBuffer = new byte[8192]; - - private readonly Socket _udpSocket; - private readonly IPEndPoint _endpoint; - private bool _disposed; - - /// <summary> - /// Initializes a new instance of the <see cref="UdpServer" /> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="appHost">The application host.</param> - /// <param name="configuration">The configuration manager.</param> - /// <param name="bindAddress"> The bind address.</param> - /// <param name="port">The port.</param> - public UdpServer( - ILogger logger, - IServerApplicationHost appHost, - IConfiguration configuration, - IPAddress bindAddress, - int port) - { - _logger = logger; - _appHost = appHost; - _config = configuration; - - _endpoint = new IPEndPoint(bindAddress, port); - - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) - { - MulticastLoopback = false, - }; - _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - - private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken) - { - string? localUrl = _config[AddressOverrideKey]; - if (string.IsNullOrEmpty(localUrl)) - { - localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); - } - - if (string.IsNullOrEmpty(localUrl)) - { - _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined."); - return; - } - - var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); - - try - { - _logger.LogDebug("Sending AutoDiscovery response"); - await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); - } - catch (SocketException ex) - { - _logger.LogError(ex, "Error sending response message"); - } - } - - /// <summary> - /// Starts the specified port. - /// </summary> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - public void Start(CancellationToken cancellationToken) - { - _udpSocket.Bind(_endpoint); - - _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); - } - - private async Task BeginReceiveAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - 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)) - { - await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); - } - } - catch (SocketException ex) - { - _logger.LogError(ex, "Failed to receive data from socket"); - } - catch (OperationCanceledException) - { - _logger.LogDebug("Broadcast socket operation cancelled"); - } - } - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - _udpSocket.Dispose(); - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index b31b4116d..15c4cfdf0 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(); } diff --git a/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs b/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs deleted file mode 100644 index d3a6ac9c8..000000000 --- a/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Emby.Dlna; -using MediaBrowser.Controller.Configuration; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; - -namespace Jellyfin.Api.Attributes; - -/// <inheritdoc /> -public sealed class DlnaEnabledAttribute : ActionFilterAttribute -{ - /// <inheritdoc /> - public override void OnActionExecuting(ActionExecutingContext context) - { - var serverConfigurationManager = context.HttpContext.RequestServices.GetRequiredService<IServerConfigurationManager>(); - - var enabled = serverConfigurationManager.GetDlnaConfiguration().EnableServer; - - if (!enabled) - { - context.Result = new StatusCodeResult(StatusCodes.Status503ServiceUnavailable); - } - } -} diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs deleted file mode 100644 index cbd32ed82..000000000 --- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc.Routing; - -namespace Jellyfin.Api.Attributes; - -/// <summary> -/// Identifies an action that supports the HTTP GET method. -/// </summary> -public sealed class HttpSubscribeAttribute : HttpMethodAttribute -{ - private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" }; - - /// <summary> - /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class. - /// </summary> - public HttpSubscribeAttribute() - : base(_supportedMethods) - { - } - - /// <summary> - /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class. - /// </summary> - /// <param name="template">The route template. May not be null.</param> - public HttpSubscribeAttribute(string template) - : base(_supportedMethods, template) - => ArgumentNullException.ThrowIfNull(template); -} diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs deleted file mode 100644 index f4a6dcdaf..000000000 --- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc.Routing; - -namespace Jellyfin.Api.Attributes; - -/// <summary> -/// Identifies an action that supports the HTTP GET method. -/// </summary> -public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute -{ - private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" }; - - /// <summary> - /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class. - /// </summary> - public HttpUnsubscribeAttribute() - : base(_supportedMethods) - { - } - - /// <summary> - /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class. - /// </summary> - /// <param name="template">The route template. May not be null.</param> - public HttpUnsubscribeAttribute(string template) - : base(_supportedMethods, template) - => ArgumentNullException.ThrowIfNull(template); -} diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 968193a6f..5bc533086 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -15,7 +15,6 @@ namespace Jellyfin.Api.Controllers; /// <summary> /// The audio controller. /// </summary> -// TODO: In order to authenticate this in the future, Dlna playback will require updating public class AudioController : BaseJellyfinApiController { private readonly AudioHelper _audioHelper; @@ -95,7 +94,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -147,7 +146,6 @@ public class AudioController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -260,7 +258,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -312,7 +310,6 @@ public class AudioController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs deleted file mode 100644 index 79a41ce3b..000000000 --- a/Jellyfin.Api/Controllers/DlnaController.cs +++ /dev/null @@ -1,133 +0,0 @@ -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; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Jellyfin.Api.Controllers; - -/// <summary> -/// Dlna Controller. -/// </summary> -[Authorize(Policy = Policies.RequiresElevation)] -public class DlnaController : BaseJellyfinApiController -{ - private readonly IDlnaManager _dlnaManager; - - /// <summary> - /// Initializes a new instance of the <see cref="DlnaController"/> class. - /// </summary> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - public DlnaController(IDlnaManager dlnaManager) - { - _dlnaManager = dlnaManager; - } - - /// <summary> - /// Get profile infos. - /// </summary> - /// <response code="200">Device profile infos returned.</response> - /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns> - [HttpGet("ProfileInfos")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos() - { - return Ok(_dlnaManager.GetProfileInfos()); - } - - /// <summary> - /// Gets the default profile. - /// </summary> - /// <response code="200">Default device profile returned.</response> - /// <returns>An <see cref="OkResult"/> containing the default profile.</returns> - [HttpGet("Profiles/Default")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<DeviceProfile> GetDefaultProfile() - { - return _dlnaManager.GetDefaultProfile(); - } - - /// <summary> - /// Gets a single profile. - /// </summary> - /// <param name="profileId">Profile Id.</param> - /// <response code="200">Device profile returned.</response> - /// <response code="404">Device profile not found.</response> - /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns> - [HttpGet("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId) - { - var profile = _dlnaManager.GetProfile(profileId); - if (profile is null) - { - return NotFound(); - } - - return profile; - } - - /// <summary> - /// Deletes a profile. - /// </summary> - /// <param name="profileId">Profile id.</param> - /// <response code="204">Device profile deleted.</response> - /// <response code="404">Device profile not found.</response> - /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> - [HttpDelete("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteProfile([FromRoute, Required] string profileId) - { - var existingDeviceProfile = _dlnaManager.GetProfile(profileId); - if (existingDeviceProfile is null) - { - return NotFound(); - } - - _dlnaManager.DeleteProfile(profileId); - return NoContent(); - } - - /// <summary> - /// Creates a profile. - /// </summary> - /// <param name="deviceProfile">Device profile.</param> - /// <response code="204">Device profile created.</response> - /// <returns>A <see cref="NoContentResult"/>.</returns> - [HttpPost("Profiles")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile) - { - _dlnaManager.CreateProfile(deviceProfile); - return NoContent(); - } - - /// <summary> - /// Updates a profile. - /// </summary> - /// <param name="profileId">Profile id.</param> - /// <param name="deviceProfile">Device profile.</param> - /// <response code="204">Device profile updated.</response> - /// <response code="404">Device profile not found.</response> - /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> - [HttpPost("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile) - { - var existingDeviceProfile = _dlnaManager.GetProfile(profileId); - if (existingDeviceProfile is null) - { - return NotFound(); - } - - _dlnaManager.UpdateProfile(profileId, deviceProfile); - return NoContent(); - } -} diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs deleted file mode 100644 index ce8d910ff..000000000 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Net.Mime; -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; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Jellyfin.Api.Controllers; - -/// <summary> -/// Dlna Server Controller. -/// </summary> -[Route("Dlna")] -[DlnaEnabled] -[Authorize(Policy = Policies.AnonymousLanAccessPolicy)] -public class DlnaServerController : BaseJellyfinApiController -{ - private readonly IDlnaManager _dlnaManager; - private readonly IContentDirectory _contentDirectory; - private readonly IConnectionManager _connectionManager; - private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar; - - /// <summary> - /// Initializes a new instance of the <see cref="DlnaServerController"/> class. - /// </summary> - /// <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, - IMediaReceiverRegistrar mediaReceiverRegistrar) - { - _dlnaManager = dlnaManager; - _contentDirectory = contentDirectory; - _connectionManager = connectionManager; - _mediaReceiverRegistrar = mediaReceiverRegistrar; - } - - /// <summary> - /// Get Description Xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Description xml returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>An <see cref="OkResult"/> containing the description xml.</returns> - [HttpGet("{serverId}/description")] - [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId) - { - var url = GetAbsoluteUri(); - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); - return Ok(xml); - } - - /// <summary> - /// Gets Dlna content directory xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Dlna content directory returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> - [HttpGet("{serverId}/ContentDirectory")] - [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] - [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId) - { - return Ok(_contentDirectory.GetServiceXml()); - } - - /// <summary> - /// Gets Dlna media receiver registrar xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Dlna media receiver registrar xml returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Dlna media receiver registrar xml.</returns> - [HttpGet("{serverId}/MediaReceiverRegistrar")] - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId) - { - return Ok(_mediaReceiverRegistrar.GetServiceXml()); - } - - /// <summary> - /// Gets Dlna media receiver registrar xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Dlna media receiver registrar xml returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Dlna media receiver registrar xml.</returns> - [HttpGet("{serverId}/ConnectionManager")] - [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] - [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId) - { - return Ok(_connectionManager.GetServiceXml()); - } - - /// <summary> - /// Process a content directory control request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Control response.</returns> - [HttpPost("{serverId}/ContentDirectory/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); - } - - /// <summary> - /// Process a connection manager control request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Control response.</returns> - [HttpPost("{serverId}/ConnectionManager/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); - } - - /// <summary> - /// Process a media receiver registrar control request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Control response.</returns> - [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); - } - - /// <summary> - /// Processes an event subscription request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Event subscription response.</returns> - [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] - [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) - { - return ProcessEventRequest(_mediaReceiverRegistrar); - } - - /// <summary> - /// Processes an event subscription request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Event subscription response.</returns> - [HttpSubscribe("{serverId}/ContentDirectory/Events")] - [HttpUnsubscribe("{serverId}/ContentDirectory/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) - { - return ProcessEventRequest(_contentDirectory); - } - - /// <summary> - /// Processes an event subscription request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Event subscription response.</returns> - [HttpSubscribe("{serverId}/ConnectionManager/Events")] - [HttpUnsubscribe("{serverId}/ConnectionManager/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) - { - return ProcessEventRequest(_connectionManager); - } - - /// <summary> - /// Gets a server icon. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <param name="fileName">The icon filename.</param> - /// <response code="200">Request processed.</response> - /// <response code="404">Not Found.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Icon stream.</returns> - [HttpGet("{serverId}/icons/{fileName}")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [ProducesImageFile] - public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) - { - return GetIconInternal(fileName); - } - - /// <summary> - /// Gets a server icon. - /// </summary> - /// <param name="fileName">The icon filename.</param> - /// <returns>Icon stream.</returns> - /// <response code="200">Request processed.</response> - /// <response code="404">Not Found.</response> - /// <response code="503">DLNA is disabled.</response> - [HttpGet("icons/{fileName}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [ProducesImageFile] - public ActionResult GetIcon([FromRoute, Required] string fileName) - { - return GetIconInternal(fileName); - } - - private ActionResult GetIconInternal(string fileName) - { - var icon = _dlnaManager.GetIcon(fileName); - if (icon is null) - { - return NotFound(); - } - - return File(icon.Stream, MimeTypes.GetMimeType(fileName)); - } - - private string GetAbsoluteUri() - { - return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"; - } - - private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) - { - return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers) - { - InputXml = requestStream, - TargetServerUuId = id, - RequestedUrl = GetAbsoluteUri() - }); - } - - private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager) - { - var subscriptionId = Request.Headers["SID"]; - if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase)) - { - var notificationType = Request.Headers["NT"]; - var callback = Request.Headers["CALLBACK"]; - var timeoutString = Request.Headers["TIMEOUT"]; - - if (string.IsNullOrEmpty(notificationType)) - { - return dlnaEventManager.RenewEventSubscription( - subscriptionId, - notificationType, - timeoutString, - callback); - } - - return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback); - } - - return dlnaEventManager.CancelEventSubscription(subscriptionId); - } -} diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 38953dc21..9e9c610cc 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -17,8 +17,6 @@ using Jellyfin.Extensions; using Jellyfin.MediaEncoding.Hls.Playlist; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Encoder; @@ -49,12 +47,10 @@ public class DynamicHlsController : BaseJellyfinApiController private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; - private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<DynamicHlsController> _logger; private readonly EncodingHelper _encodingHelper; @@ -67,12 +63,10 @@ public class DynamicHlsController : BaseJellyfinApiController /// </summary> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> @@ -81,12 +75,10 @@ public class DynamicHlsController : BaseJellyfinApiController public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, - IDlnaManager dlnaManager, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, ILogger<DynamicHlsController> logger, DynamicHlsHelper dynamicHlsHelper, @@ -95,12 +87,10 @@ public class DynamicHlsController : BaseJellyfinApiController { _libraryManager = libraryManager; _userManager = userManager; - _dlnaManager = dlnaManager; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; - _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; @@ -176,7 +166,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -231,7 +221,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -294,8 +283,6 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, TranscodingJobType, cancellationToken) @@ -422,7 +409,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -477,7 +464,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -594,7 +580,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -647,7 +633,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -760,7 +745,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -814,7 +799,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -928,7 +912,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -981,7 +965,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -1105,7 +1088,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -1161,7 +1144,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -1286,7 +1268,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -1341,7 +1323,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -1402,8 +1383,6 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, TranscodingJobType, cancellationTokenSource.Token) @@ -1442,8 +1421,6 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, TranscodingJobType, cancellationToken) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index f0e578e7a..fdebb3d45 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -91,12 +91,6 @@ public class SessionController : BaseJellyfinApiController result = result.Where(i => !i.UserId.Equals(default)); } - if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - result = result.Where(i => { if (!string.IsNullOrWhiteSpace(i.DeviceId)) @@ -111,6 +105,12 @@ public class SessionController : BaseJellyfinApiController }); } + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } + return Ok(result); } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 7aa5d01e2..5d9868eb9 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -16,8 +16,6 @@ using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -42,11 +40,9 @@ public class VideosController : BaseJellyfinApiController private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; - private readonly IDlnaManager _dlnaManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; private readonly EncodingHelper _encodingHelper; @@ -59,11 +55,9 @@ public class VideosController : BaseJellyfinApiController /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> @@ -71,11 +65,9 @@ public class VideosController : BaseJellyfinApiController ILibraryManager libraryManager, IUserManager userManager, IDtoService dtoService, - IDlnaManager dlnaManager, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, IHttpClientFactory httpClientFactory, EncodingHelper encodingHelper) @@ -83,11 +75,9 @@ public class VideosController : BaseJellyfinApiController _libraryManager = libraryManager; _userManager = userManager; _dtoService = dtoService; - _dlnaManager = dlnaManager; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; _encodingHelper = encodingHelper; @@ -324,7 +314,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -381,7 +371,6 @@ public class VideosController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -438,8 +427,6 @@ public class VideosController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, _transcodingJobType, cancellationTokenSource.Token) @@ -447,8 +434,6 @@ public class VideosController : BaseJellyfinApiController if (@static.HasValue && @static.Value && state.DirectStreamProvider is not null) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); - var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); if (liveStreamInfo is null) { @@ -463,8 +448,6 @@ public class VideosController : BaseJellyfinApiController // Static remote stream if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); - var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false); } @@ -475,12 +458,6 @@ public class VideosController : BaseJellyfinApiController } var outputPath = state.OutputFilePath; - var outputPathExists = System.IO.File.Exists(outputPath); - - var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); - var isTranscodeCached = outputPathExists && transcodingJob is not null; - - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager); // Static stream if (@static.HasValue && @static.Value) diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index 2b18c389d..926ce99dd 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -7,8 +7,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.MediaInfo; @@ -23,13 +21,11 @@ namespace Jellyfin.Api.Helpers; /// </summary> public class AudioHelper { - private readonly IDlnaManager _dlnaManager; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpContextAccessor _httpContextAccessor; @@ -38,37 +34,31 @@ public class AudioHelper /// <summary> /// Initializes a new instance of the <see cref="AudioHelper"/> class. /// </summary> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public AudioHelper( - IDlnaManager dlnaManager, IUserManager userManager, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor, EncodingHelper encodingHelper) { - _dlnaManager = dlnaManager; _userManager = userManager; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; _httpContextAccessor = httpContextAccessor; @@ -104,8 +94,6 @@ public class AudioHelper _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, transcodingJobType, cancellationTokenSource.Token) @@ -113,8 +101,6 @@ public class AudioHelper if (streamingRequest.Static && state.DirectStreamProvider is not null) { - StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); - var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); if (liveStreamInfo is null) { @@ -129,8 +115,6 @@ public class AudioHelper // Static remote stream if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); - var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); } @@ -141,12 +125,6 @@ public class AudioHelper } var outputPath = state.OutputFilePath; - var outputPathExists = File.Exists(outputPath); - - var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); - var isTranscodeCached = outputPathExists && transcodingJob is not null; - - StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); // Static stream if (streamingRequest.Static) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index a8df628f0..05f7d44bf 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -16,8 +16,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Trickplay; @@ -38,11 +36,9 @@ public class DynamicHlsHelper { private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly INetworkManager _networkManager; private readonly ILogger<DynamicHlsHelper> _logger; @@ -55,11 +51,9 @@ public class DynamicHlsHelper /// </summary> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> @@ -69,11 +63,9 @@ public class DynamicHlsHelper public DynamicHlsHelper( ILibraryManager libraryManager, IUserManager userManager, - IDlnaManager dlnaManager, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, INetworkManager networkManager, ILogger<DynamicHlsHelper> logger, @@ -83,11 +75,9 @@ public class DynamicHlsHelper { _libraryManager = libraryManager; _userManager = userManager; - _dlnaManager = dlnaManager; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _networkManager = networkManager; _logger = logger; @@ -140,8 +130,6 @@ public class DynamicHlsHelper _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, transcodingJobType, cancellationTokenSource.Token) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 7d9a38931..71c62b235 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -12,15 +12,12 @@ using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers; @@ -41,8 +38,6 @@ public static class StreamingHelpers /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param> /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> @@ -56,21 +51,11 @@ public static class StreamingHelpers IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, EncodingHelper encodingHelper, - IDlnaManager dlnaManager, - IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, TranscodingJobType transcodingJobType, CancellationToken cancellationToken) { var httpRequest = httpContext.Request; - // Parse the DLNA time seek header - if (!streamingRequest.StartTimeTicks.HasValue) - { - var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"]; - - streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString()); - } - if (!string.IsNullOrWhiteSpace(streamingRequest.Params)) { ParseParams(streamingRequest); @@ -89,16 +74,11 @@ public static class StreamingHelpers streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url); } - var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) || - streamingRequest.StreamOptions.ContainsKey("dlnaheaders") || - string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase); - var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) { Request = streamingRequest, RequestedUrl = url, - UserAgent = httpRequest.Headers[HeaderNames.UserAgent], - EnableDlnaHeaders = enableDlnaHeaders + UserAgent = httpRequest.Headers[HeaderNames.UserAgent] }; var userId = httpContext.User.GetUserId(); @@ -243,8 +223,6 @@ public static class StreamingHelpers } } - ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static); - var ext = string.IsNullOrWhiteSpace(state.OutputContainer) ? GetOutputFileExtension(state, mediaSource) : ("." + state.OutputContainer); @@ -255,123 +233,6 @@ public static class StreamingHelpers } /// <summary> - /// Adds the dlna headers. - /// </summary> - /// <param name="state">The state.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param> - /// <param name="startTimeTicks">The start time in ticks.</param> - /// <param name="request">The <see cref="HttpRequest"/>.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - public static void AddDlnaHeaders( - StreamState state, - IHeaderDictionary responseHeaders, - bool isStaticallyStreamed, - long? startTimeTicks, - HttpRequest request, - IDlnaManager dlnaManager) - { - if (!state.EnableDlnaHeaders) - { - return; - } - - var profile = state.DeviceProfile; - - StringValues transferMode = request.Headers["transferMode.dlna.org"]; - 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.Append("MediaInfo.sec", string.Format( - CultureInfo.InvariantCulture, - "SEC_Duration={0};", - Convert.ToInt32(ms))); - } - - if (!isStaticallyStreamed && profile is not null) - { - AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks); - } - } - - profile ??= dlnaManager.GetDefaultProfile(); - - var audioCodec = state.ActualOutputAudioCodec; - - if (!state.IsVideoRequest) - { - responseHeaders.Append("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader( - profile, - state.OutputContainer, - audioCodec, - state.OutputAudioBitrate, - state.OutputAudioSampleRate, - state.OutputAudioChannels, - state.OutputAudioBitDepth, - isStaticallyStreamed, - state.RunTimeTicks, - state.TranscodeSeekInfo)); - } - else - { - var videoCodec = state.ActualOutputVideoCodec; - - 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); - } - } - - /// <summary> - /// Parses the time seek header. - /// </summary> - /// <param name="value">The time seek header string.</param> - /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns> - private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value) - { - if (value.IsEmpty) - { - return null; - } - - const string npt = "npt="; - if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("Invalid timeseek header"); - } - - var index = value.IndexOf('-'); - value = index == -1 - ? value.Slice(npt.Length) - : value.Slice(npt.Length, index - npt.Length); - if (!value.Contains(':')) - { - // Parses npt times in the format of '417.33' - if (double.TryParse(value, CultureInfo.InvariantCulture, out var seconds)) - { - return TimeSpan.FromSeconds(seconds).Ticks; - } - - throw new ArgumentException("Invalid timeseek header"); - } - - try - { - // Parses npt times in the format of '10:19:25.7' - return TimeSpan.Parse(value, CultureInfo.InvariantCulture).Ticks; - } - catch - { - throw new ArgumentException("Invalid timeseek header"); - } - } - - /// <summary> /// Parses query parameters as StreamOptions. /// </summary> /// <param name="queryString">The query string.</param> @@ -394,29 +255,6 @@ public static class StreamingHelpers } /// <summary> - /// Adds the dlna time seek headers to the response. - /// </summary> - /// <param name="state">The current <see cref="StreamState"/>.</param> - /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param> - /// <param name="startTimeTicks">The start time in ticks.</param> - private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks) - { - var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); - var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); - - responseHeaders.Append("TimeSeekRange.dlna.org", string.Format( - CultureInfo.InvariantCulture, - "npt={0}-{1}/{1}", - startSeconds, - runtimeSeconds)); - responseHeaders.Append("X-AvailableSeekRange", string.Format( - CultureInfo.InvariantCulture, - "1 npt={0}-{1}", - startSeconds, - runtimeSeconds)); - } - - /// <summary> /// Gets the output file extension. /// </summary> /// <param name="state">The state.</param> @@ -519,79 +357,6 @@ public static class StreamingHelpers return Path.Combine(folder, filename + ext); } - private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static) - { - if (!string.IsNullOrWhiteSpace(deviceProfileId)) - { - state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId); - - if (state.DeviceProfile is null) - { - var caps = deviceManager.GetCapabilities(deviceProfileId); - state.DeviceProfile = caps is null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile; - } - } - - var profile = state.DeviceProfile; - - if (profile is null) - { - // Don't use settings from the default profile. - // Only use a specific profile if it was requested. - return; - } - - var audioCodec = state.ActualOutputAudioCodec; - var videoCodec = state.ActualOutputVideoCodec; - - var mediaProfile = !state.IsVideoRequest - ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth) - : profile.GetVideoMediaProfile( - state.OutputContainer, - audioCodec, - videoCodec, - state.OutputWidth, - state.OutputHeight, - state.TargetVideoBitDepth, - state.OutputVideoBitrate, - state.TargetVideoProfile, - state.TargetVideoRangeType, - state.TargetVideoLevel, - state.TargetFramerate, - state.TargetPacketLength, - state.TargetTimestamp, - state.IsTargetAnamorphic, - state.IsTargetInterlaced, - state.TargetRefFrames, - state.TargetVideoStreamCount, - state.TargetAudioStreamCount, - state.TargetVideoCodecTag, - state.IsTargetAVC); - - if (mediaProfile is not null) - { - state.MimeType = mediaProfile.MimeType; - } - - if (!(@static.HasValue && @static.Value)) - { - var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec); - - if (transcodingProfile is not null) - { - state.EstimateContentLength = transcodingProfile.EstimateContentLength; - // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; - state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; - - if (state.VideoRequest is not null) - { - state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; - state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; - } - } - } - } - /// <summary> /// Parses the parameters. /// </summary> @@ -619,7 +384,7 @@ public static class StreamingHelpers switch (i) { case 0: - request.DeviceProfileId = val; + // DeviceProfileId break; case 1: request.DeviceId = val; diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 2473fb288..5f86a6b6b 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -18,10 +18,10 @@ </ItemGroup> <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" /> + <ProjectReference Include="..\src\Jellyfin.Networking\Jellyfin.Networking.csproj" /> </ItemGroup> <!-- Code Analyzers --> diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index f249f3bc6..cc1f9163e 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -139,16 +139,6 @@ public class StreamState : EncodingJobInfo, IDisposable public TranscodeSeekInfo TranscodeSeekInfo { get; set; } /// <summary> - /// Gets or sets a value indicating whether to enable dlna headers. - /// </summary> - public bool EnableDlnaHeaders { get; set; } - - /// <summary> - /// Gets or sets the device profile. - /// </summary> - public DeviceProfile? DeviceProfile { get; set; } - - /// <summary> /// Gets or sets the transcoding job. /// </summary> public TranscodingJobDto? TranscodingJob { get; set; } diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs index 389d6006d..a357498d4 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs @@ -8,11 +8,6 @@ namespace Jellyfin.Api.Models.StreamingDtos; public class StreamingRequestDto : BaseEncodingJobOptions { /// <summary> - /// Gets or sets the device profile. - /// </summary> - public string? DeviceProfileId { get; set; } - - /// <summary> /// Gets or sets the params. /// </summary> public string? Params { get; set; } diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs deleted file mode 100644 index d59e4e5e3..000000000 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ /dev/null @@ -1,120 +0,0 @@ -/* -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; -using System.Threading; -using System.Threading.Tasks; - -namespace Jellyfin.Networking.HappyEyeballs -{ - /// <summary> - /// Defines the <see cref="HttpClientExtension"/> class. - /// - /// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 . - /// </summary> - public static class HttpClientExtension - { - /// <summary> - /// Gets or sets a value indicating whether the client should use IPv6. - /// </summary> - public static bool UseIPv6 { get; set; } = true; - - /// <summary> - /// Implements the httpclient callback method. - /// </summary> - /// <param name="context">The <see cref="SocketsHttpConnectionContext"/> instance.</param> - /// <param name="cancellationToken">The <see cref="CancellationToken"/> instance.</param> - /// <returns>The http steam.</returns> - public static async ValueTask<Stream> OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) - { - if (!UseIPv6) - { - return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); - } - - using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); - - // 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) - { - await cancelIPv6.CancelAsync().ConfigureAwait(false); - 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) - { - await cancelIPv4.CancelAsync().ConfigureAwait(false); - return tryConnectAsyncIPv6.GetAwaiter().GetResult(); - } - - return tryConnectAsyncIPv4.GetAwaiter().GetResult(); - } - else - { - if (tryConnectAsyncIPv4.IsCompletedSuccessfully) - { - await cancelIPv6.CancelAsync().ConfigureAwait(false); - return tryConnectAsyncIPv4.GetAwaiter().GetResult(); - } - - return tryConnectAsyncIPv6.GetAwaiter().GetResult(); - } - } - - private static async Task<Stream> 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/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj deleted file mode 100644 index 30f41aeb2..000000000 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ /dev/null @@ -1,31 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> - <GenerateAssemblyInfo>false</GenerateAssemblyInfo> - <GenerateDocumentationFile>true</GenerateDocumentationFile> - </PropertyGroup> - - <ItemGroup> - <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> - - <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> - <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> - </ItemGroup> -</Project> diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs deleted file mode 100644 index 749e0abbb..000000000 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ /dev/null @@ -1,1126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading; -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 -{ - /// <summary> - /// Class to take care of network interface management. - /// </summary> - public class NetworkManager : INetworkManager, IDisposable - { - /// <summary> - /// Threading lock for network properties. - /// </summary> - private readonly object _initLock; - - private readonly ILogger<NetworkManager> _logger; - - 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 IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls; - - 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 IReadOnlyList<IPData> _interfaces; - - /// <summary> - /// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>) - /// or internal interface network subnets if undefined by user. - /// </summary> - private IReadOnlyList<IPNetwork> _lanSubnets; - - /// <summary> - /// User defined list of subnets to excluded from the LAN. - /// </summary> - private IReadOnlyList<IPNetwork> _excludedSubnets; - - /// <summary> - /// True if this object is disposed. - /// </summary> - private bool _disposed; - - /// <summary> - /// Initializes a new instance of the <see cref="NetworkManager"/> class. - /// </summary> - /// <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, 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 List<PublishedServerUriOverride>(); - _networkEventLock = new object(); - _remoteAddressFilter = new List<IPNetwork>(); - - UpdateSettings(_configurationManager.GetNetworkConfiguration()); - - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - - _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated; - } -#pragma warning restore CS8618 // Non-nullable field is uninitialized. - - /// <summary> - /// Event triggered on network changes. - /// </summary> - public event EventHandler? NetworkChanged; - - /// <summary> - /// Gets or sets a value indicating whether testing is taking place. - /// </summary> - public static string MockNetworkSettings { get; set; } = string.Empty; - - /// <summary> - /// Gets a value indicating whether IP4 is enabled. - /// </summary> - public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4; - - /// <summary> - /// Gets a value indicating whether IP6 is enabled. - /// </summary> - public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6; - - /// <summary> - /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. - /// </summary> - public bool TrustAllIPv6Interfaces { get; private set; } - - /// <summary> - /// Gets the Published server override list. - /// </summary> - public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls; - - /// <inheritdoc/> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Handler for network change events. - /// </summary> - /// <param name="sender">Sender.</param> - /// <param name="e">A <see cref="NetworkAvailabilityEventArgs"/> containing network availability information.</param> - private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) - { - _logger.LogDebug("Network availability changed."); - HandleNetworkChange(); - } - - /// <summary> - /// Handler for network change events. - /// </summary> - /// <param name="sender">Sender.</param> - /// <param name="e">An <see cref="EventArgs"/>.</param> - private void OnNetworkAddressChanged(object? sender, EventArgs e) - { - _logger.LogDebug("Network address change detected."); - HandleNetworkChange(); - } - - /// <summary> - /// Triggers our event, and re-loads interface information. - /// </summary> - private void HandleNetworkChange() - { - lock (_networkEventLock) - { - if (!_eventfire) - { - // As network events tend to fire one after the other only fire once every second. - _eventfire = true; - OnNetworkChange(); - } - } - } - - /// <summary> - /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. - /// </summary> - private void OnNetworkChange() - { - try - { - Thread.Sleep(2000); - var networkConfig = _configurationManager.GetNetworkConfiguration(); - if (IsIPv6Enabled && !Socket.OSSupportsIPv6) - { - UpdateSettings(networkConfig); - } - else - { - InitializeInterfaces(); - InitializeLan(networkConfig); - EnforceBindSettings(networkConfig); - } - - PrintNetworkInformation(networkConfig); - NetworkChanged?.Invoke(this, EventArgs.Empty); - } - finally - { - _eventfire = false; - } - } - - /// <summary> - /// 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 InitializeInterfaces() - { - lock (_initLock) - { - _logger.LogDebug("Refreshing interfaces."); - - var interfaces = new List<IPData>(); - var macAddresses = new List<PhysicalAddress>(); - - try - { - var nics = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.OperationalStatus == OperationalStatus.Up); - - foreach (NetworkInterface adapter in nics) - { - try - { - var ipProperties = adapter.GetIPProperties(); - var mac = adapter.GetPhysicalAddress(); - - // Populate MAC list - if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac)) - { - macAddresses.Add(mac); - } - - // Populate interface list - foreach (var info in ipProperties.UnicastAddresses) - { - if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) - { - 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) - { - Index = ipProperties.GetIPv6Properties().Index, - Name = adapter.Name, - SupportsMulticast = adapter.SupportsMulticast - }; - - interfaces.Add(interfaceObject); - } - } - } - catch (Exception ex) - { - // Ignore error, and attempt to continue. - _logger.LogError(ex, "Error encountered parsing interfaces."); - } - } - } - catch (Exception ex) - { - _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)."); - - if (IsIPv4Enabled) - { - interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); - } - - if (IsIPv6Enabled) - { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "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())); - - _macAddresses = macAddresses; - _interfaces = interfaces; - } - } - - /// <summary> - /// Initializes internal LAN cache. - /// </summary> - private void InitializeLan(NetworkConfiguration config) - { - lock (_initLock) - { - _logger.LogDebug("Refreshing LAN information."); - - // Get configuration options - var subnets = config.LocalNetworkSubnets; - - // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN - if (!NetworkUtils.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) - { - 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(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; - } - else - { - _lanSubnets = lanSubnets; - } - - _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true) - ? excludedSubnets - : new List<IPNetwork>(); - } - } - - /// <summary> - /// Enforce bind addresses and exclusions on available interfaces. - /// </summary> - private void EnforceBindSettings(NetworkConfiguration config) - { - 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 => NetworkUtils.TryParseToSubnet(p, out var network) - ? network.Prefix - : (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(); - - if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) - { - 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, NetworkConstants.IPv6RFC4291Loopback, "lo")); - } - } - - // Remove all interfaces matching any virtual machine interface prefix - if (config.IgnoreVirtualInterfaces) - { - // Remove potentially existing * and split config string into prefixes - var virtualInterfacePrefixes = config.VirtualInterfaceNames - .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); - - // Check all interfaces for matches against the prefixes and remove them - if (_interfaces.Count > 0) - { - foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) - { - interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); - } - } - } - - // 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); - } - - _interfaces = interfaces; - } - } - - /// <summary> - /// Initializes the remote address values. - /// </summary> - private void InitializeRemote(NetworkConfiguration config) - { - lock (_initLock) - { - // Parse config values into filter collection - var remoteIPFilter = config.RemoteIPFilter; - if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0])) - { - // Parse all IPs with netmask to a subnet - var remoteAddressFilter = new List<IPNetwork>(); - var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(); - if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) - { - remoteAddressFilter = remoteAddressFilterResult.ToList(); - } - - // Parse everything else as an IP and construct subnet with a single IP - var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase)); - foreach (var ip in remoteFilteredIPs) - { - if (IPAddress.TryParse(ip, out var ipp)) - { - remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize)); - } - } - - _remoteAddressFilter = remoteAddressFilter; - } - } - } - - /// <summary> - /// 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. - /// </summary> - private void InitializeOverrides(NetworkConfiguration config) - { - lock (_initLock) - { - var publishedServerUrls = new List<PublishedServerUriOverride>(); - - // Prefer startup configuration. - var startupOverrideKey = _startupConfig[AddressOverrideKey]; - if (!string.IsNullOrEmpty(startupOverrideKey)) - { - publishedServerUrls.Add( - new PublishedServerUriOverride( - new IPData(IPAddress.Any, NetworkConstants.IPv4Any), - startupOverrideKey, - true, - true)); - publishedServerUrls.Add( - new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), - startupOverrideKey, - true, - true)); - _publishedServerUrls = publishedServerUrls; - return; - } - - var 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); - return; - } - - var replacement = parts[1].Trim(); - var identifier = parts[0]; - if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) - { - // Drop any other overrides in case an "all" override exists - publishedServerUrls.Clear(); - publishedServerUrls.Add( - new PublishedServerUriOverride( - new IPData(IPAddress.Any, NetworkConstants.IPv4Any), - replacement, - true, - true)); - publishedServerUrls.Add( - new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), - replacement, - true, - true)); - break; - } - else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) - { - publishedServerUrls.Add( - new PublishedServerUriOverride( - new IPData(IPAddress.Any, NetworkConstants.IPv4Any), - replacement, - false, - true)); - publishedServerUrls.Add( - new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), - replacement, - false, - true)); - } - else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) - { - foreach (var lan in _lanSubnets) - { - var lanPrefix = lan.Prefix; - publishedServerUrls.Add( - new PublishedServerUriOverride( - new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), - replacement, - true, - false)); - } - } - else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) - { - var data = new IPData(result.Prefix, result); - publishedServerUrls.Add( - new PublishedServerUriOverride( - data, - replacement, - true, - true)); - } - else if (TryParseInterface(identifier, out var ifaces)) - { - foreach (var iface in ifaces) - { - publishedServerUrls.Add( - new PublishedServerUriOverride( - iface, - replacement, - true, - true)); - } - } - else - { - _logger.LogError("Unable to parse bind override: {Entry}", entry); - } - } - - _publishedServerUrls = publishedServerUrls; - } - } - - private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) - { - if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal)) - { - UpdateSettings((NetworkConfiguration)evt.NewConfiguration); - } - } - - /// <summary> - /// Reloads all settings and re-Initializes the instance. - /// </summary> - /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param> - public void UpdateSettings(object configuration) - { - ArgumentNullException.ThrowIfNull(configuration); - - var config = (NetworkConfiguration)configuration; - HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; - - InitializeLan(config); - InitializeRemote(config); - - if (string.IsNullOrEmpty(MockNetworkSettings)) - { - InitializeInterfaces(); - } - else // Used in testing only. - { - // 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(','); - if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) - { - 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]) - { - Index = index - }; - interfaces.Add(data); - } - } - else - { - _logger.LogWarning("Could not parse mock interface settings: {Part}", details); - } - } - - _interfaces = interfaces; - } - - EnforceBindSettings(config); - InitializeOverrides(config); - - PrintNetworkInformation(config, false); - } - - /// <summary> - /// Protected implementation of Dispose pattern. - /// </summary> - /// <param name="disposing"><c>True</c> to dispose the managed state.</param> - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; - NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; - } - - _disposed = true; - } - } - - /// <inheritdoc/> - public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result) - { - if (string.IsNullOrEmpty(intf) - || _interfaces is null - || _interfaces.Count == 0) - { - result = null; - return false; - } - - // Match all interfaces starting with names starting with token - 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/> - 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.Any(x => x.Contains(remoteIP))) - { - // Remote not enabled. So everyone should be LAN. - return false; - } - - return true; - } - - /// <inheritdoc/> - public IReadOnlyList<PhysicalAddress> GetMacAddresses() - { - // Populated in construction - so always has values. - return _macAddresses; - } - - /// <inheritdoc/> - public IReadOnlyList<IPData> GetLoopbacks() - { - if (!IsIPv4Enabled && !IsIPv6Enabled) - { - return Array.Empty<IPData>(); - } - - var loopbackNetworks = new List<IPData>(); - if (IsIPv4Enabled) - { - loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); - } - - if (IsIPv6Enabled) - { - loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); - } - - return loopbackNetworks; - } - - /// <inheritdoc/> - public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) - { - if (_interfaces.Count > 0 || individualInterfaces) - { - return _interfaces; - } - - // No bind address and no exclusions, so listen on all interfaces. - var result = new List<IPData>(); - 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, NetworkConstants.IPv6Any)); - } - else if (IsIPv4Enabled) - { - result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any)); - } - 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 GetBindAddress(string source, out int? port) - { - if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) - { - addresses = Array.Empty<IPAddress>(); - } - - var result = GetBindAddress(addresses.FirstOrDefault(), out port); - return result; - } - - /// <inheritdoc/> - public string GetBindAddress(HttpRequest source, out int? port) - { - var result = GetBindAddress(source.Host.Host, out port); - port ??= source.Host.Port; - - return result; - } - - /// <inheritdoc/> - public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false) - { - port = null; - - string result; - - if (source is not null) - { - 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) - { - _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("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal); - - if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result)) - { - return result; - } - - // 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. - // 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) - .ToList(); - - if (availableInterfaces.Count == 0) - { - // 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; - } - - // If no source address is given, use the preferred (first) interface - if (source is null) - { - result = NetworkUtils.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 = 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 = NetworkUtils.FormatIPString(availableInterfaces[0].Address); - _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); - return result; - } - - /// <inheritdoc/> - public IReadOnlyList<IPData> GetInternalBindAddresses() - { - // Select all local bind addresses - return _interfaces.Where(x => IsInLocalNetwork(x.Address)) - .OrderBy(x => x.Index) - .ToList(); - } - - /// <inheritdoc/> - public bool IsInLocalNetwork(string address) - { - 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 (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) - { - foreach (var ept in addresses) - { - if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept)))) - { - return true; - } - } - } - - return false; - } - - /// <inheritdoc/> - public bool IsInLocalNetwork(IPAddress address) - { - ArgumentNullException.ThrowIfNull(address); - - // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. - 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 - return CheckIfLanAndNotExcluded(address); - } - - private bool CheckIfLanAndNotExcluded(IPAddress address) - { - foreach (var lanSubnet in _lanSubnets) - { - if (lanSubnet.Contains(address)) - { - foreach (var excludedSubnet in _excludedSubnets) - { - if (excludedSubnet.Contains(address)) - { - return false; - } - } - - return true; - } - } - - return false; - } - - /// <summary> - /// Attempts to match the source against the published server URL overrides. - /// </summary> - /// <param name="source">IP source address to use.</param> - /// <param name="isInExternalSubnet">True if the source is in an external subnet.</param> - /// <param name="bindPreference">The published server URL that matches the source address.</param> - /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns> - private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference) - { - bindPreference = string.Empty; - int? port = null; - - // 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(); - } - - foreach (var data in validPublishedServerUrls) - { - // 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) - { - // If matching interface is found, use override - bindPreference = data.OverrideUri; - break; - } - } - - if (string.IsNullOrEmpty(bindPreference)) - { - _logger.LogDebug("{Source}: No matching bind address override found", source); - return false; - } - - // Handle override specifying port - var parts = bindPreference.Split(':'); - if (parts.Length > 1) - { - if (int.TryParse(parts[1], out int p)) - { - bindPreference = parts[0]; - port = p; - _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); - return true; - } - } - - _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); - return true; - } - - /// <summary> - /// Attempts to match the source against the user defined bind interfaces. - /// </summary> - /// <param name="source">IP source address to use.</param> - /// <param name="isInExternalSubnet">True if the source is in the external subnet.</param> - /// <param name="result">The result, if a match is found.</param> - /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns> - private bool MatchesBindInterface(IPAddress source, bool isInExternalSubnet, out string result) - { - result = string.Empty; - - 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) - { - return false; - } - - IPAddress? bindAddress = null; - if (isInExternalSubnet) - { - var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address)) - .OrderBy(x => x.Index) - .ToList(); - if (externalInterfaces.Count > 0) - { - // 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) - .First(); - - result = NetworkUtils.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)) - .OrderByDescending(x => x.Subnet.Contains(source)) - .ThenBy(x => x.Index) - .Select(x => x.Address) - .FirstOrDefault(); - - if (bindAddress is not null) - { - result = NetworkUtils.FormatIPString(bindAddress); - _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); - return true; - } - } - - return false; - } - - /// <summary> - /// Attempts to match the source against external interfaces. - /// </summary> - /// <param name="source">IP source address to use.</param> - /// <param name="result">The result, if a match is found.</param> - /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns> - private bool MatchesExternalInterface(IPAddress source, out string result) - { - // 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; - } - - // Does the request originate in one of the interface subnets? - // (For systems with multiple network cards and/or multiple subnets) - foreach (var intf in extResult) - { - if (intf.Subnet.Contains(source)) - { - 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 = NetworkUtils.FormatIPString(extResult[0].Address); - _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.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index ce1c54cbb..54272aeaf 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)); } @@ -80,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); } } diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index b2dfe60a1..07ac27e3c 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); } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 49f5bf232..aa7be9109 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -4,7 +4,6 @@ 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.HappyEyeballs; @@ -122,7 +121,6 @@ namespace Jellyfin.Server .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext)); services.AddHlsPlaylistGenerator(); - services.AddDlnaServices(_serverApplicationHost); } /// <summary> diff --git a/Jellyfin.sln b/Jellyfin.sln index cad23fc5e..4385ac241 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -23,10 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\ 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}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" @@ -63,9 +59,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "src\Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}" EndProject @@ -139,14 +133,6 @@ Global {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 {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.Build.0 = Release|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.Build.0 = Release|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -199,10 +185,6 @@ Global {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU - {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.Build.0 = Release|Any CPU {30922383-D513-4F4D-B890-A940B57FA353}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {30922383-D513-4F4D-B890-A940B57FA353}.Debug|Any CPU.Build.0 = Debug|Any CPU {30922383-D513-4F4D-B890-A940B57FA353}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -262,7 +244,6 @@ Global {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} @@ -277,6 +258,7 @@ Global {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index c51090e38..78a391d36 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -82,7 +82,7 @@ namespace MediaBrowser.Common.Net /// <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, bool skipOverrides = false); + 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) diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs deleted file mode 100644 index 06da5ea09..000000000 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ /dev/null @@ -1,80 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Dlna; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Controller.Dlna -{ - public interface IDlnaManager - { - /// <summary> - /// Gets the profile infos. - /// </summary> - /// <returns>IEnumerable{DeviceProfileInfo}.</returns> - IEnumerable<DeviceProfileInfo> GetProfileInfos(); - - /// <summary> - /// Gets the profile. - /// </summary> - /// <param name="headers">The headers.</param> - /// <returns>DeviceProfile.</returns> - DeviceProfile? GetProfile(IHeaderDictionary headers); - - /// <summary> - /// Gets the default profile. - /// </summary> - /// <returns>DeviceProfile.</returns> - DeviceProfile GetDefaultProfile(); - - /// <summary> - /// Creates the profile. - /// </summary> - /// <param name="profile">The profile.</param> - void CreateProfile(DeviceProfile profile); - - /// <summary> - /// Updates the profile. - /// </summary> - /// <param name="profileId">The profile id.</param> - /// <param name="profile">The profile.</param> - void UpdateProfile(string profileId, DeviceProfile profile); - - /// <summary> - /// Deletes the profile. - /// </summary> - /// <param name="id">The identifier.</param> - void DeleteProfile(string id); - - /// <summary> - /// Gets the profile. - /// </summary> - /// <param name="id">The identifier.</param> - /// <returns>DeviceProfile.</returns> - DeviceProfile? GetProfile(string id); - - /// <summary> - /// Gets the profile. - /// </summary> - /// <param name="deviceInfo">The device information.</param> - /// <returns>DeviceProfile.</returns> - DeviceProfile? GetProfile(DeviceIdentification deviceInfo); - - /// <summary> - /// Gets the server description XML. - /// </summary> - /// <param name="headers">The headers.</param> - /// <param name="serverUuId">The server uu identifier.</param> - /// <param name="serverAddress">The server address.</param> - /// <returns>System.String.</returns> - string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress); - - /// <summary> - /// Gets the icon. - /// </summary> - /// <param name="filename">The filename.</param> - /// <returns>DlnaIconResponse.</returns> - ImageStream? GetIcon(string filename); - } -} diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 63f9180fd..66dea1084 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -25,6 +25,9 @@ namespace MediaBrowser.Controller.Entities public override bool SupportsPositionTicksResume => true; [JsonIgnore] + public override bool SupportsPeople => true; + + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } [JsonIgnore] diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 46fd1ae47..6a16d421c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3343,7 +3343,7 @@ namespace MediaBrowser.Controller.MediaEncoding // [0:s]scale=s=1280x720 var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } return (mainFilters, subFilters, overlayFilters); @@ -3520,7 +3520,7 @@ namespace MediaBrowser.Controller.MediaEncoding } subFilters.Add("hwupload=derive_device=cuda"); - overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0"); } } else @@ -3529,7 +3529,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -3718,7 +3718,7 @@ namespace MediaBrowser.Controller.MediaEncoding } subFilters.Add("hwupload=derive_device=opencl"); - overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0"); overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1"); overlayFilters.Add("format=d3d11"); } @@ -3729,7 +3729,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -3964,7 +3964,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_qsv=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayQsvFilter); } @@ -3975,7 +3975,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -4180,7 +4180,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_qsv=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayQsvFilter); } @@ -4191,7 +4191,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -4445,7 +4445,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayVaapiFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_vaapi=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_vaapi=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayVaapiFilter); } @@ -4456,7 +4456,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); if (isVaapiEncoder) { @@ -4616,7 +4616,7 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=vulkan"); subFilters.Add("format=vulkan"); - overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_vulkan=eof_action=pass:repeatlast=0"); if (isSwEncoder) { @@ -4817,7 +4817,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); if (isVaapiEncoder) { diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs deleted file mode 100644 index f29022b54..000000000 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ /dev/null @@ -1,259 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using Jellyfin.Data.Enums; -using MediaBrowser.Model.MediaInfo; - -namespace MediaBrowser.Model.Dlna -{ - public static class ContentFeatureBuilder - { - public static string BuildImageHeader( - DeviceProfile profile, - string container, - int? width, - int? height, - bool isDirectStream, - string orgPn = null) - { - string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetImageOrgOpValue(); - - // 0 = native, 1 = transcoded - var orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; - - var flagValue = DlnaFlags.BackgroundTransferMode | - DlnaFlags.InteractiveTransferMode | - DlnaFlags.DlnaV15; - - string dlnaflags = string.Format( - CultureInfo.InvariantCulture, - ";DLNA.ORG_FLAGS={0}", - DlnaMaps.FlagsToString(flagValue)); - - if (string.IsNullOrEmpty(orgPn)) - { - ResponseProfile mediaProfile = profile.GetImageMediaProfile( - container, - width, - height); - - orgPn = mediaProfile?.OrgPn; - - if (string.IsNullOrEmpty(orgPn)) - { - orgPn = GetImageOrgPnValue(container, width, height); - } - } - - if (string.IsNullOrEmpty(orgPn)) - { - return orgOp.TrimStart(';') + orgCi + dlnaflags; - } - - return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags; - } - - public static string BuildAudioHeader( - DeviceProfile profile, - string container, - string audioCodec, - int? audioBitrate, - int? audioSampleRate, - int? audioChannels, - int? audioBitDepth, - bool isDirectStream, - long? runtimeTicks, - TranscodeSeekInfo transcodeSeekInfo) - { - // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none - string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo); - - // 0 = native, 1 = transcoded - string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; - - var flagValue = DlnaFlags.StreamingTransferMode | - DlnaFlags.BackgroundTransferMode | - DlnaFlags.InteractiveTransferMode | - DlnaFlags.DlnaV15; - - // if (isDirectStream) - // { - // flagValue = flagValue | DlnaFlags.ByteBasedSeek; - // } - // else if (runtimeTicks.HasValue) - // { - // flagValue = flagValue | DlnaFlags.TimeBasedSeek; - // } - - string dlnaflags = string.Format( - CultureInfo.InvariantCulture, - ";DLNA.ORG_FLAGS={0}", - DlnaMaps.FlagsToString(flagValue)); - - ResponseProfile mediaProfile = profile.GetAudioMediaProfile( - container, - audioCodec, - audioChannels, - audioBitrate, - audioSampleRate, - audioBitDepth); - - string orgPn = mediaProfile?.OrgPn; - - if (string.IsNullOrEmpty(orgPn)) - { - orgPn = GetAudioOrgPnValue(container, audioBitrate, audioSampleRate, audioChannels); - } - - if (string.IsNullOrEmpty(orgPn)) - { - return orgOp.TrimStart(';') + orgCi + dlnaflags; - } - - return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags; - } - - public static IEnumerable<string> BuildVideoHeader( - DeviceProfile profile, - string container, - string videoCodec, - string audioCodec, - int? width, - int? height, - int? bitDepth, - int? videoBitrate, - TransportStreamTimestamp timestamp, - bool isDirectStream, - long? runtimeTicks, - string videoProfile, - VideoRangeType videoRangeType, - double? videoLevel, - float? videoFramerate, - int? packetLength, - TranscodeSeekInfo transcodeSeekInfo, - bool? isAnamorphic, - bool? isInterlaced, - int? refFrames, - int? numVideoStreams, - int? numAudioStreams, - string videoCodecTag, - bool? isAvc) - { - // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none - string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo); - - // 0 = native, 1 = transcoded - string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; - - var flagValue = DlnaFlags.StreamingTransferMode | - DlnaFlags.BackgroundTransferMode | - DlnaFlags.InteractiveTransferMode | - DlnaFlags.DlnaV15; - - if (isDirectStream) - { - flagValue |= DlnaFlags.ByteBasedSeek; - } - - // Time based seek is currently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths. - // else if (runtimeTicks.HasValue) - // { - // flagValue = flagValue | DlnaFlags.TimeBasedSeek; - // } - - string dlnaflags = string.Format( - CultureInfo.InvariantCulture, - ";DLNA.ORG_FLAGS={0}", - DlnaMaps.FlagsToString(flagValue)); - - ResponseProfile mediaProfile = profile.GetVideoMediaProfile( - container, - audioCodec, - videoCodec, - width, - height, - bitDepth, - videoBitrate, - videoProfile, - videoRangeType, - videoLevel, - videoFramerate, - packetLength, - timestamp, - isAnamorphic, - isInterlaced, - refFrames, - numVideoStreams, - numAudioStreams, - videoCodecTag, - isAvc); - - var orgPnValues = new List<string>(); - - if (mediaProfile is not null && !string.IsNullOrEmpty(mediaProfile.OrgPn)) - { - orgPnValues.AddRange(mediaProfile.OrgPn.Split(',', StringSplitOptions.RemoveEmptyEntries)); - } - else - { - foreach (var s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp)) - { - orgPnValues.Add(s.ToString()); - break; - } - } - - var contentFeatureList = new List<string>(); - - foreach (string orgPn in orgPnValues) - { - if (string.IsNullOrEmpty(orgPn)) - { - contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags); - } - else if (isDirectStream) - { - // orgOp should be added all the time once the time based seek is resolved for transcoded streams - contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags); - } - else - { - contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgCi + dlnaflags); - } - } - - if (orgPnValues.Count == 0) - { - contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags); - } - - return contentFeatureList; - } - - private static string GetImageOrgPnValue(string container, int? width, int? height) - { - MediaFormatProfile? format = MediaFormatProfileResolver.ResolveImageFormat(container, width, height); - - return format.HasValue ? format.Value.ToString() : null; - } - - private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) - { - MediaFormatProfile? format = MediaFormatProfileResolver.ResolveAudioFormat( - container, - audioBitrate, - audioSampleRate, - audioChannels); - - return format.HasValue ? format.Value.ToString() : null; - } - - private static MediaFormatProfile[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) - { - return MediaFormatProfileResolver.ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp); - } - } -} diff --git a/MediaBrowser.Model/Dlna/DlnaFlags.cs b/MediaBrowser.Model/Dlna/DlnaFlags.cs deleted file mode 100644 index 02d9ea9c5..000000000 --- a/MediaBrowser.Model/Dlna/DlnaFlags.cs +++ /dev/null @@ -1,50 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Dlna -{ - [Flags] - public enum DlnaFlags : ulong - { - /*! <i>Background</i> transfer mode. - For use with upload and download transfers to and from the server. - The primary difference between \ref DH_TransferMode_Interactive and - \ref DH_TransferMode_Bulk is that the latter assumes that the user - is not relying on the transfer for immediately rendering the content - and there are no issues with causing a buffer overflow if the - receiver uses TCP flow control to reduce total throughput. - */ - BackgroundTransferMode = 1 << 22, - - ByteBasedSeek = 1 << 29, - ConnectionStall = 1 << 21, - - DlnaV15 = 1 << 20, - - /*! <i>Interactive</i> transfer mode. - For best effort transfer of images and non-real-time transfers. - URIs with image content usually support \ref DH_TransferMode_Bulk too. - The primary difference between \ref DH_TransferMode_Interactive and - \ref DH_TransferMode_Bulk is that the former assumes that the - transfer is intended for immediate rendering. - */ - InteractiveTransferMode = 1 << 23, - - PlayContainer = 1 << 28, - RtspPause = 1 << 25, - S0Increase = 1 << 27, - SenderPaced = 1L << 31, - SnIncrease = 1 << 26, - - /*! <i>Streaming</i> transfer mode. - The server transmits at a throughput sufficient for real-time playback of - audio or video. URIs with audio or video often support the - \ref DH_TransferMode_Interactive and \ref DH_TransferMode_Bulk transfer modes. - The most well-known exception to this general claim is for live streams. - */ - StreamingTransferMode = 1 << 24, - - TimeBasedSeek = 1 << 30 - } -} diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs deleted file mode 100644 index 4613bc542..000000000 --- a/MediaBrowser.Model/Dlna/DlnaMaps.cs +++ /dev/null @@ -1,46 +0,0 @@ -#pragma warning disable CS1591 - -using System.Globalization; - -namespace MediaBrowser.Model.Dlna -{ - public static class DlnaMaps - { - public static string FlagsToString(DlnaFlags flags) - { - return string.Format(CultureInfo.InvariantCulture, "{0:X8}{1:D24}", (ulong)flags, 0); - } - - public static string GetOrgOpValue(bool hasKnownRuntime, bool isDirectStream, TranscodeSeekInfo profileTranscodeSeekInfo) - { - if (hasKnownRuntime) - { - string orgOp = string.Empty; - - // Time-based seeking currently only possible when transcoding - orgOp += isDirectStream ? "0" : "1"; - - // Byte-based seeking only possible when not transcoding - orgOp += isDirectStream || profileTranscodeSeekInfo == TranscodeSeekInfo.Bytes ? "1" : "0"; - - return orgOp; - } - - // No seeking is available if we don't know the content runtime - return "00"; - } - - public static string GetImageOrgOpValue() - { - string orgOp = string.Empty; - - // Time-based seeking currently only possible when transcoding - orgOp += "0"; - - // Byte-based seeking only possible when not transcoding - orgOp += "0"; - - return orgOp; - } - } -} diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs deleted file mode 100644 index 086088dea..000000000 --- a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using Jellyfin.Data.Events; - -namespace MediaBrowser.Model.Dlna -{ - public interface IDeviceDiscovery - { - event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered; - - event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft; - } -} diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs deleted file mode 100644 index 06f6660f4..000000000 --- a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs +++ /dev/null @@ -1,114 +0,0 @@ -#pragma warning disable CS1591, CA1707 - -namespace MediaBrowser.Model.Dlna -{ - public enum MediaFormatProfile - { - MP3, - WMA_BASE, - WMA_FULL, - LPCM16_44_MONO, - LPCM16_44_STEREO, - LPCM16_48_MONO, - LPCM16_48_STEREO, - AAC_ISO, - AAC_ISO_320, - AAC_ADTS, - AAC_ADTS_320, - FLAC, - OGG, - - JPEG_SM, - JPEG_MED, - JPEG_LRG, - JPEG_TN, - PNG_LRG, - PNG_TN, - GIF_LRG, - RAW, - - MPEG1, - MPEG_PS_PAL, - MPEG_PS_NTSC, - MPEG_TS_SD_EU, - MPEG_TS_SD_EU_ISO, - MPEG_TS_SD_EU_T, - MPEG_TS_SD_NA, - MPEG_TS_SD_NA_ISO, - MPEG_TS_SD_NA_T, - MPEG_TS_SD_KO, - MPEG_TS_SD_KO_ISO, - MPEG_TS_SD_KO_T, - MPEG_TS_JP_T, - AVI, - MATROSKA, - FLV, - DVR_MS, - WTV, - OGV, - AVC_MP4_MP_SD_AAC_MULT5, - AVC_MP4_MP_SD_MPEG1_L3, - AVC_MP4_MP_SD_AC3, - AVC_MP4_MP_HD_720p_AAC, - AVC_MP4_MP_HD_1080i_AAC, - AVC_MP4_HP_HD_AAC, - AVC_TS_MP_HD_AAC_MULT5, - AVC_TS_MP_HD_AAC_MULT5_T, - AVC_TS_MP_HD_AAC_MULT5_ISO, - AVC_TS_MP_HD_MPEG1_L3, - AVC_TS_MP_HD_MPEG1_L3_T, - AVC_TS_MP_HD_MPEG1_L3_ISO, - AVC_TS_MP_HD_AC3, - AVC_TS_MP_HD_AC3_T, - AVC_TS_MP_HD_AC3_ISO, - AVC_TS_HP_HD_MPEG1_L2_T, - AVC_TS_HP_HD_MPEG1_L2_ISO, - AVC_TS_MP_SD_AAC_MULT5, - AVC_TS_MP_SD_AAC_MULT5_T, - AVC_TS_MP_SD_AAC_MULT5_ISO, - AVC_TS_MP_SD_MPEG1_L3, - AVC_TS_MP_SD_MPEG1_L3_T, - AVC_TS_MP_SD_MPEG1_L3_ISO, - AVC_TS_HP_SD_MPEG1_L2_T, - AVC_TS_HP_SD_MPEG1_L2_ISO, - AVC_TS_MP_SD_AC3, - AVC_TS_MP_SD_AC3_T, - AVC_TS_MP_SD_AC3_ISO, - AVC_TS_HD_DTS_T, - AVC_TS_HD_DTS_ISO, - WMVMED_BASE, - WMVMED_FULL, - WMVMED_PRO, - WMVHIGH_FULL, - WMVHIGH_PRO, - VC1_ASF_AP_L1_WMA, - VC1_ASF_AP_L2_WMA, - VC1_ASF_AP_L3_WMA, - VC1_TS_AP_L1_AC3_ISO, - VC1_TS_AP_L2_AC3_ISO, - VC1_TS_HD_DTS_ISO, - VC1_TS_HD_DTS_T, - MPEG4_P2_MP4_ASP_AAC, - MPEG4_P2_MP4_SP_L6_AAC, - MPEG4_P2_MP4_NDSD, - MPEG4_P2_TS_ASP_AAC, - MPEG4_P2_TS_ASP_AAC_T, - MPEG4_P2_TS_ASP_AAC_ISO, - MPEG4_P2_TS_ASP_MPEG1_L3, - MPEG4_P2_TS_ASP_MPEG1_L3_T, - MPEG4_P2_TS_ASP_MPEG1_L3_ISO, - MPEG4_P2_TS_ASP_MPEG2_L2, - MPEG4_P2_TS_ASP_MPEG2_L2_T, - MPEG4_P2_TS_ASP_MPEG2_L2_ISO, - MPEG4_P2_TS_ASP_AC3, - MPEG4_P2_TS_ASP_AC3_T, - MPEG4_P2_TS_ASP_AC3_ISO, - AVC_TS_HD_50_LPCM_T, - AVC_MP4_LPCM, - MPEG4_P2_3GPP_SP_L0B_AAC, - MPEG4_P2_3GPP_SP_L0B_AMR, - AVC_3GPP_BL_QCIF15_AAC, - MPEG4_H263_3GPP_P0_L10_AMR, - MPEG4_H263_MP4_P0_L10_AAC - } -} diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs deleted file mode 100644 index 93a9ae615..000000000 --- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs +++ /dev/null @@ -1,532 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using MediaBrowser.Model.MediaInfo; - -namespace MediaBrowser.Model.Dlna -{ - public static class MediaFormatProfileResolver - { - public static MediaFormatProfile[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) - { - if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) - { - MediaFormatProfile? val = ResolveVideoASFFormat(videoCodec, audioCodec, width, height); - return val.HasValue ? new MediaFormatProfile[] { val.Value } : Array.Empty<MediaFormatProfile>(); - } - - if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase)) - { - MediaFormatProfile? val = ResolveVideoMP4Format(videoCodec, audioCodec, width, height); - return val.HasValue ? new MediaFormatProfile[] { val.Value } : Array.Empty<MediaFormatProfile>(); - } - - if (string.Equals(container, "avi", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.AVI }; - } - - if (string.Equals(container, "mkv", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.MATROSKA }; - } - - if (string.Equals(container, "mpeg2ps", StringComparison.OrdinalIgnoreCase) || - string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.MPEG_PS_NTSC, MediaFormatProfile.MPEG_PS_PAL }; - } - - if (string.Equals(container, "mpeg1video", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.MPEG1 }; - } - - if (string.Equals(container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) || - string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) || - string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase)) - { - return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType); - } - - if (string.Equals(container, "flv", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.FLV }; - } - - if (string.Equals(container, "wtv", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.WTV }; - } - - if (string.Equals(container, "3gp", StringComparison.OrdinalIgnoreCase)) - { - MediaFormatProfile? val = ResolveVideo3GPFormat(videoCodec, audioCodec); - return val.HasValue ? new MediaFormatProfile[] { val.Value } : Array.Empty<MediaFormatProfile>(); - } - - if (string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.OGV }; - } - - return Array.Empty<MediaFormatProfile>(); - } - - private static MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) - { - string suffix = string.Empty; - - switch (timestampType) - { - case TransportStreamTimestamp.None: - suffix = "_ISO"; - break; - case TransportStreamTimestamp.Valid: - suffix = "_T"; - break; - } - - string resolution = "S"; - if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576)) - { - resolution = "H"; - } - - if (string.Equals(videoCodec, "mpeg2video", StringComparison.OrdinalIgnoreCase)) - { - var list = new List<MediaFormatProfile> - { - ValueOf("MPEG_TS_SD_NA" + suffix), - ValueOf("MPEG_TS_SD_EU" + suffix), - ValueOf("MPEG_TS_SD_KO" + suffix) - }; - - if ((timestampType == TransportStreamTimestamp.Valid) && string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - list.Add(MediaFormatProfile.MPEG_TS_JP_T); - } - - return list.ToArray(); - } - - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(audioCodec, "lpcm", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_50_LPCM_T }; - } - - if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)) - { - if (timestampType == TransportStreamTimestamp.None) - { - return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_ISO }; - } - - return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_T }; - } - - if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase)) - { - if (timestampType == TransportStreamTimestamp.None) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_ISO", resolution)) }; - } - - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) }; - } - - if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) }; - } - - if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) }; - } - - if (string.IsNullOrEmpty(audioCodec) || - string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) }; - } - } - else if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) - { - if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576)) - { - return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L2_AC3_ISO }; - } - - return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L1_AC3_ISO }; - } - - if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)) - { - suffix = string.Equals(suffix, "_ISO", StringComparison.OrdinalIgnoreCase) ? suffix : "_T"; - - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "VC1_TS_HD_DTS{0}", suffix)) }; - } - } - else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AAC{0}", suffix)) }; - } - - if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) }; - } - - if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) }; - } - - if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) - { - return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AC3{0}", suffix)) }; - } - } - - return Array.Empty<MediaFormatProfile>(); - } - - private static MediaFormatProfile ValueOf(string value) - { - return (MediaFormatProfile)Enum.Parse(typeof(MediaFormatProfile), value, true); - } - - private static MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height) - { - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(audioCodec, "lpcm", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.AVC_MP4_LPCM; - } - - if (string.IsNullOrEmpty(audioCodec) || - string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.AVC_MP4_MP_SD_AC3; - } - - if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.AVC_MP4_MP_SD_MPEG1_L3; - } - - if (width.HasValue && height.HasValue) - { - if ((width.Value <= 720) && (height.Value <= 576)) - { - if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.AVC_MP4_MP_SD_AAC_MULT5; - } - } - else if ((width.Value <= 1280) && (height.Value <= 720)) - { - if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.AVC_MP4_MP_HD_720p_AAC; - } - } - else if ((width.Value <= 1920) && (height.Value <= 1080)) - { - if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.AVC_MP4_MP_HD_1080i_AAC; - } - } - } - } - else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) - { - if (width.HasValue && height.HasValue && width.Value <= 720 && height.Value <= 576) - { - if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MPEG4_P2_MP4_ASP_AAC; - } - - if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MPEG4_P2_MP4_NDSD; - } - } - else if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MPEG4_P2_MP4_SP_L6_AAC; - } - } - else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MPEG4_H263_MP4_P0_L10_AAC; - } - - return null; - } - - private static MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec) - { - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.AVC_3GPP_BL_QCIF15_AAC; - } - } - else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AAC; - } - - if (string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AMR; - } - } - else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MPEG4_H263_3GPP_P0_L10_AMR; - } - - return null; - } - - private static MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height) - { - if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) && - (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase))) - { - if (width.HasValue && height.HasValue) - { - if ((width.Value <= 720) && (height.Value <= 576)) - { - if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.WMVMED_FULL; - } - - return MediaFormatProfile.WMVMED_PRO; - } - } - - if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.WMVHIGH_FULL; - } - - return MediaFormatProfile.WMVHIGH_PRO; - } - - if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase)) - { - if (width.HasValue && height.HasValue) - { - if ((width.Value <= 720) && (height.Value <= 576)) - { - return MediaFormatProfile.VC1_ASF_AP_L1_WMA; - } - - if ((width.Value <= 1280) && (height.Value <= 720)) - { - return MediaFormatProfile.VC1_ASF_AP_L2_WMA; - } - - if ((width.Value <= 1920) && (height.Value <= 1080)) - { - return MediaFormatProfile.VC1_ASF_AP_L3_WMA; - } - } - } - else if (string.Equals(videoCodec, "mpeg2video", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.DVR_MS; - } - - return null; - } - - public static MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels) - { - if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) - { - return ResolveAudioASFFormat(bitrate); - } - - if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.MP3; - } - - if (string.Equals(container, "lpcm", StringComparison.OrdinalIgnoreCase)) - { - return ResolveAudioLPCMFormat(frequency, channels); - } - - if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase) || - string.Equals(container, "aac", StringComparison.OrdinalIgnoreCase)) - { - return ResolveAudioMP4Format(bitrate); - } - - if (string.Equals(container, "adts", StringComparison.OrdinalIgnoreCase)) - { - return ResolveAudioADTSFormat(bitrate); - } - - if (string.Equals(container, "flac", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.FLAC; - } - - if (string.Equals(container, "oga", StringComparison.OrdinalIgnoreCase) || - string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.OGG; - } - - return null; - } - - private static MediaFormatProfile ResolveAudioASFFormat(int? bitrate) - { - if (bitrate.HasValue && bitrate.Value <= 193) - { - return MediaFormatProfile.WMA_BASE; - } - - return MediaFormatProfile.WMA_FULL; - } - - private static MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels) - { - if (frequency.HasValue && channels.HasValue) - { - if (frequency.Value == 44100 && channels.Value == 1) - { - return MediaFormatProfile.LPCM16_44_MONO; - } - - if (frequency.Value == 44100 && channels.Value == 2) - { - return MediaFormatProfile.LPCM16_44_STEREO; - } - - if (frequency.Value == 48000 && channels.Value == 1) - { - return MediaFormatProfile.LPCM16_48_MONO; - } - - if (frequency.Value == 48000 && channels.Value == 2) - { - return MediaFormatProfile.LPCM16_48_STEREO; - } - - return null; - } - - return MediaFormatProfile.LPCM16_48_STEREO; - } - - private static MediaFormatProfile ResolveAudioMP4Format(int? bitrate) - { - if (bitrate.HasValue && bitrate.Value <= 320) - { - return MediaFormatProfile.AAC_ISO_320; - } - - return MediaFormatProfile.AAC_ISO; - } - - private static MediaFormatProfile ResolveAudioADTSFormat(int? bitrate) - { - if (bitrate.HasValue && bitrate.Value <= 320) - { - return MediaFormatProfile.AAC_ADTS_320; - } - - return MediaFormatProfile.AAC_ADTS; - } - - public static MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height) - { - if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) || - string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase)) - { - return ResolveImageJPGFormat(width, height); - } - - if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase)) - { - return ResolveImagePNGFormat(width, height); - } - - if (string.Equals(container, "gif", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.GIF_LRG; - } - - if (string.Equals(container, "raw", StringComparison.OrdinalIgnoreCase)) - { - return MediaFormatProfile.RAW; - } - - return null; - } - - private static MediaFormatProfile ResolveImageJPGFormat(int? width, int? height) - { - if (width.HasValue && height.HasValue) - { - if ((width.Value <= 160) && (height.Value <= 160)) - { - return MediaFormatProfile.JPEG_TN; - } - - if ((width.Value <= 640) && (height.Value <= 480)) - { - return MediaFormatProfile.JPEG_SM; - } - - if ((width.Value <= 1024) && (height.Value <= 768)) - { - return MediaFormatProfile.JPEG_MED; - } - - return MediaFormatProfile.JPEG_LRG; - } - - return MediaFormatProfile.JPEG_SM; - } - - private static MediaFormatProfile ResolveImagePNGFormat(int? width, int? height) - { - if (width.HasValue && height.HasValue) - { - if ((width.Value <= 160) && (height.Value <= 160)) - { - return MediaFormatProfile.PNG_TN; - } - } - - return MediaFormatProfile.PNG_LRG; - } - } -} diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs deleted file mode 100644 index 6f4a692c8..000000000 --- a/MediaBrowser.Model/Dlna/SearchCriteria.cs +++ /dev/null @@ -1,55 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Text.RegularExpressions; - -namespace MediaBrowser.Model.Dlna -{ - public partial class SearchCriteria - { - public SearchCriteria(string search) - { - ArgumentException.ThrowIfNullOrEmpty(search); - - SearchType = SearchType.Unknown; - - string[] factors = AndOrRegex().Split(search); - foreach (string factor in factors) - { - string[] subFactors = WhiteSpaceRegex().Split(factor.Trim().Trim('(').Trim(')').Trim(), 3); - - if (subFactors.Length == 3) - { - if (string.Equals("upnp:class", subFactors[0], StringComparison.OrdinalIgnoreCase) - && (string.Equals("=", subFactors[1], StringComparison.Ordinal) || string.Equals("derivedfrom", subFactors[1], StringComparison.OrdinalIgnoreCase))) - { - if (string.Equals("\"object.item.imageItem\"", subFactors[2], StringComparison.Ordinal) || string.Equals("\"object.item.imageItem.photo\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) - { - SearchType = SearchType.Image; - } - else if (string.Equals("\"object.item.videoItem\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) - { - SearchType = SearchType.Video; - } - else if (string.Equals("\"object.container.playlistContainer\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) - { - SearchType = SearchType.Playlist; - } - else if (string.Equals("\"object.container.album.musicAlbum\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) - { - SearchType = SearchType.MusicAlbum; - } - } - } - } - } - - public SearchType SearchType { get; set; } - - [GeneratedRegex("\\s")] - private static partial Regex WhiteSpaceRegex(); - - [GeneratedRegex("(and|or)", RegexOptions.IgnoreCase)] - private static partial Regex AndOrRegex(); - } -} diff --git a/MediaBrowser.Model/Dlna/SearchType.cs b/MediaBrowser.Model/Dlna/SearchType.cs deleted file mode 100644 index 8bc7c5249..000000000 --- a/MediaBrowser.Model/Dlna/SearchType.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Dlna -{ - public enum SearchType - { - Unknown = 0, - Audio = 1, - Image = 2, - Video = 3, - Playlist = 4, - MusicAlbum = 5 - } -} diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs deleted file mode 100644 index 7df53c6d1..000000000 --- a/MediaBrowser.Model/Dlna/SortCriteria.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using Jellyfin.Data.Enums; - -namespace MediaBrowser.Model.Dlna -{ - public class SortCriteria - { - public SortCriteria(string sortOrder) - { - if (Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue)) - { - SortOrder = sortOrderValue; - } - else - { - SortOrder = SortOrder.Ascending; - } - } - - public SortOrder SortOrder { get; } - } -} diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs deleted file mode 100644 index c7489d57a..000000000 --- a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net; - -namespace MediaBrowser.Model.Dlna -{ - public class UpnpDeviceInfo - { - public Uri Location { get; set; } - - public Dictionary<string, string> Headers { get; set; } - - public IPAddress LocalIPAddress { get; set; } - - public int LocalPort { get; set; } - - public IPAddress RemoteIPAddress { get; set; } - } -} diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index 128034eb8..62b87d9f5 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -14,22 +14,4 @@ public interface ISocketFactory /// <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 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/MediaBrowser.Model/Providers/ExternalIdMediaType.cs b/MediaBrowser.Model/Providers/ExternalIdMediaType.cs index 5303c8f58..ef518369c 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 } } diff --git a/RSSDP/DeviceAvailableEventArgs.cs b/RSSDP/DeviceAvailableEventArgs.cs deleted file mode 100644 index f933f258b..000000000 --- a/RSSDP/DeviceAvailableEventArgs.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Net; - -namespace Rssdp -{ - /// <summary> - /// Event arguments for the <see cref="Infrastructure.SsdpDeviceLocator.DeviceAvailable"/> event. - /// </summary> - public sealed class DeviceAvailableEventArgs : EventArgs - { - public IPAddress RemoteIPAddress { get; set; } - - private readonly DiscoveredSsdpDevice _DiscoveredDevice; - - private readonly bool _IsNewlyDiscovered; - - /// <summary> - /// Full constructor. - /// </summary> - /// <param name="discoveredDevice">A <see cref="DiscoveredSsdpDevice"/> instance representing the available device.</param> - /// <param name="isNewlyDiscovered">A boolean value indicating whether or not this device came from the cache. See <see cref="IsNewlyDiscovered"/> for more detail.</param> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="discoveredDevice"/> parameter is null.</exception> - public DeviceAvailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool isNewlyDiscovered) - { - if (discoveredDevice == null) - { - throw new ArgumentNullException(nameof(discoveredDevice)); - } - - _DiscoveredDevice = discoveredDevice; - _IsNewlyDiscovered = isNewlyDiscovered; - } - - /// <summary> - /// Returns true if the device was discovered due to an alive notification, or a search and was not already in the cache. Returns false if the item came from the cache but matched the current search request. - /// </summary> - public bool IsNewlyDiscovered - { - get { return _IsNewlyDiscovered; } - } - - /// <summary> - /// A reference to a <see cref="DiscoveredSsdpDevice"/> instance containing the discovered details and allowing access to the full device description. - /// </summary> - public DiscoveredSsdpDevice DiscoveredDevice - { - get { return _DiscoveredDevice; } - } - } -} diff --git a/RSSDP/DeviceEventArgs.cs b/RSSDP/DeviceEventArgs.cs deleted file mode 100644 index 2455ccbfa..000000000 --- a/RSSDP/DeviceEventArgs.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Rssdp -{ - /// <summary> - /// Event arguments for the <see cref="SsdpDevice.DeviceAdded"/> and <see cref="SsdpDevice.DeviceRemoved"/> events. - /// </summary> - public sealed class DeviceEventArgs : EventArgs - { - private readonly SsdpDevice _Device; - - /// <summary> - /// Constructs a new instance for the specified <see cref="SsdpDevice"/>. - /// </summary> - /// <param name="device">The <see cref="SsdpDevice"/> associated with the event this argument class is being used for.</param> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - public DeviceEventArgs(SsdpDevice device) - { - if (device == null) - { - throw new ArgumentNullException(nameof(device)); - } - - _Device = device; - } - - /// <summary> - /// Returns the <see cref="SsdpDevice"/> instance the event being raised for. - /// </summary> - public SsdpDevice Device - { - get { return _Device; } - } - } -} diff --git a/RSSDP/DeviceUnavailableEventArgs.cs b/RSSDP/DeviceUnavailableEventArgs.cs deleted file mode 100644 index ca2515202..000000000 --- a/RSSDP/DeviceUnavailableEventArgs.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace Rssdp -{ - /// <summary> - /// Event arguments for the <see cref="Infrastructure.SsdpDeviceLocator.DeviceUnavailable"/> event. - /// </summary> - public sealed class DeviceUnavailableEventArgs : EventArgs - { - private readonly DiscoveredSsdpDevice _DiscoveredDevice; - - private readonly bool _Expired; - - /// <summary> - /// Full constructor. - /// </summary> - /// <param name="discoveredDevice">A <see cref="DiscoveredSsdpDevice"/> instance representing the device that has become unavailable.</param> - /// <param name="expired">A boolean value indicating whether this device is unavailable because it expired, or because it explicitly sent a byebye notification.. See <see cref="Expired"/> for more detail.</param> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="discoveredDevice"/> parameter is null.</exception> - public DeviceUnavailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool expired) - { - if (discoveredDevice == null) - { - throw new ArgumentNullException(nameof(discoveredDevice)); - } - - _DiscoveredDevice = discoveredDevice; - _Expired = expired; - } - - /// <summary> - /// Returns true if the device is considered unavailable because it's cached information expired before a new alive notification or search result was received. Returns false if the device is unavailable because it sent an explicit notification of it's unavailability. - /// </summary> - public bool Expired - { - get { return _Expired; } - } - - /// <summary> - /// A reference to a <see cref="DiscoveredSsdpDevice"/> instance containing the discovery details of the removed device. - /// </summary> - public DiscoveredSsdpDevice DiscoveredDevice - { - get { return _DiscoveredDevice; } - } - } -} diff --git a/RSSDP/DiscoveredSsdpDevice.cs b/RSSDP/DiscoveredSsdpDevice.cs deleted file mode 100644 index 322bd55e5..000000000 --- a/RSSDP/DiscoveredSsdpDevice.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Net.Http.Headers; - -namespace Rssdp -{ - /// <summary> - /// Represents a discovered device, containing basic information about the device and the location of it's full device description document. Also provides convenience methods for retrieving the device description document. - /// </summary> - /// <seealso cref="SsdpDevice"/> - /// <seealso cref="Infrastructure.ISsdpDeviceLocator"/> - public sealed class DiscoveredSsdpDevice - { - private DateTimeOffset _AsAt; - - /// <summary> - /// Sets or returns the type of notification, being either a uuid, device type, service type or upnp:rootdevice. - /// </summary> - public string NotificationType { get; set; } - - /// <summary> - /// Sets or returns the universal service name (USN) of the device. - /// </summary> - public string Usn { get; set; } - - /// <summary> - /// Sets or returns a URL pointing to the device description document for this device. - /// </summary> - public Uri DescriptionLocation { get; set; } - - /// <summary> - /// Sets or returns the length of time this information is valid for (from the <see cref="AsAt"/> time). - /// </summary> - public TimeSpan CacheLifetime { get; set; } - - /// <summary> - /// Sets or returns the date and time this information was received. - /// </summary> - public DateTimeOffset AsAt - { - get { return _AsAt; } - - set - { - if (_AsAt != value) - { - _AsAt = value; - } - } - } - - /// <summary> - /// Returns the headers from the SSDP device response message. - /// </summary> - public HttpHeaders ResponseHeaders { get; set; } - - /// <summary> - /// Returns true if this device information has expired, based on the current date/time, and the <see cref="CacheLifetime"/> & <see cref="AsAt"/> properties. - /// </summary> - /// <returns></returns> - public bool IsExpired() - { - return this.CacheLifetime == TimeSpan.Zero || this.AsAt.Add(this.CacheLifetime) <= DateTimeOffset.Now; - } - - /// <summary> - /// Returns the device's <see cref="Usn"/> value. - /// </summary> - /// <returns>A string containing the device's universal service name.</returns> - public override string ToString() - { - return this.Usn; - } - } -} diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs deleted file mode 100644 index 5d7da4124..000000000 --- a/RSSDP/DisposableManagedObjectBase.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Correctly implements the <see cref="IDisposable"/> interface and pattern for an object containing only managed resources, and adds a few common niceties not on the interface such as an <see cref="IsDisposed"/> property. - /// </summary> - public abstract class DisposableManagedObjectBase : IDisposable - { - /// <summary> - /// Override this method and dispose any objects you own the lifetime of if disposing is true; - /// </summary> - /// <param name="disposing">True if managed objects should be disposed, if false, only unmanaged resources should be released.</param> - protected abstract void Dispose(bool disposing); - - /// <summary> - /// Throws and <see cref="ObjectDisposedException"/> if the <see cref="IsDisposed"/> property is true. - /// </summary> - /// <seealso cref="IsDisposed"/> - /// <exception cref="ObjectDisposedException">Thrown if the <see cref="IsDisposed"/> property is true.</exception> - /// <seealso cref="Dispose()"/> - protected virtual void ThrowIfDisposed() - { - if (this.IsDisposed) - { - throw new ObjectDisposedException(this.GetType().FullName); - } - } - - /// <summary> - /// Sets or returns a boolean indicating whether or not this instance has been disposed. - /// </summary> - /// <seealso cref="Dispose()"/> - public bool IsDisposed - { - get; - private set; - } - - public string BuildMessage(string header, Dictionary<string, string> values) - { - var builder = new StringBuilder(); - - const string ArgFormat = "{0}: {1}\r\n"; - - builder.AppendFormat(CultureInfo.InvariantCulture, "{0}\r\n", header); - - foreach (var pair in values) - { - builder.AppendFormat(CultureInfo.InvariantCulture, ArgFormat, pair.Key, pair.Value); - } - - builder.Append("\r\n"); - - return builder.ToString(); - } - - /// <summary> - /// Disposes this object instance and all internally managed resources. - /// </summary> - /// <remarks> - /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behavior of derived classes.</para> - /// </remarks> - /// <seealso cref="IsDisposed"/> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfere with the dispose process.")] - public void Dispose() - { - IsDisposed = true; - - Dispose(true); - } - } -} diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs deleted file mode 100644 index 1949a9df3..000000000 --- a/RSSDP/HttpParserBase.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// A base class for the <see cref="HttpResponseParser"/> and <see cref="HttpRequestParser"/> classes. Not intended for direct use. - /// </summary> - /// <typeparam name="T"></typeparam> - public abstract class HttpParserBase<T> where T : new() - { - private readonly string[] LineTerminators = new string[] { "\r\n", "\n" }; - private readonly char[] SeparatorCharacters = new char[] { ',', ';' }; - - /// <summary> - /// Parses the <paramref name="data"/> provided into either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object. - /// </summary> - /// <param name="data">A string containing the HTTP message to parse.</param> - /// <returns>Either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object containing the parsed data.</returns> - public abstract T Parse(string data); - - /// <summary> - /// Parses a string containing either an HTTP request or response into a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object. - /// </summary> - /// <param name="message">A <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object representing the parsed message.</param> - /// <param name="headers">A reference to the <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the <paramref name="message"/> object.</param> - /// <param name="data">A string containing the data to be parsed.</param> - /// <returns>An <see cref="HttpContent"/> object containing the content of the parsed message.</returns> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Honestly, it's fine. MemoryStream doesn't mind.")] - protected virtual void Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (data.Length == 0) - { - throw new ArgumentException("data cannot be an empty string.", nameof(data)); - } - - if (!LineTerminators.Any(data.Contains)) - { - throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", nameof(data)); - } - - using (var retVal = new ByteArrayContent(Array.Empty<byte>())) - { - var lines = data.Split(LineTerminators, StringSplitOptions.None); - - // First line is the 'request' line containing http protocol details like method, uri, http version etc. - ParseStatusLine(lines[0], message); - - ParseHeaders(headers, retVal.Headers, lines); - } - } - - /// <summary> - /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>. - /// </summary> - /// <param name="data">The first line of the HTTP message to be parsed.</param> - /// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param> - protected abstract void ParseStatusLine(string data, T message); - - /// <summary> - /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). - /// </summary> - /// <param name="headerName">A string containing the name of the header to return the type of.</param> - protected abstract bool IsContentHeader(string headerName); - - /// <summary> - /// Parses the HTTP version text from an HTTP request or response status line and returns a <see cref="Version"/> object representing the parsed values. - /// </summary> - /// <param name="versionData">A string containing the HTTP version, from the message status line.</param> - /// <returns>A <see cref="Version"/> object containing the parsed version data.</returns> - protected Version ParseHttpVersion(string versionData) - { - if (versionData == null) - { - throw new ArgumentNullException(nameof(versionData)); - } - - var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal); - if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length) - { - throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData)); - } - - return Version.Parse(versionData.Substring(versionSeparatorIndex + 1)); - } - - /// <summary> - /// Parses a line from an HTTP request or response message containing a header name and value pair. - /// </summary> - /// <param name="line">A string containing the data to be parsed.</param> - /// <param name="headers">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection to which the parsed header will be added.</param> - /// <param name="contentHeaders">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the message content, to which the parsed header will be added.</param> - private void ParseHeader(string line, System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders) - { - // Header format is - // name: value - var headerKeySeparatorIndex = line.IndexOf(':', StringComparison.Ordinal); - var headerName = line.Substring(0, headerKeySeparatorIndex).Trim(); - var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim(); - - // Not sure how to determine where request headers and content headers begin, - // at least not without a known set of headers (general headers first the content headers) - // which seems like a bad way of doing it. So we'll assume if it's a known content header put it there - // else use request headers. - - var values = ParseValues(headerValue); - var headersToAddTo = IsContentHeader(headerName) ? contentHeaders : headers; - - if (values.Count > 1) - { - headersToAddTo.TryAddWithoutValidation(headerName, values); - } - else - { - headersToAddTo.TryAddWithoutValidation(headerName, values[0]); - } - } - - private int ParseHeaders(System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders, string[] lines) - { - // Blank line separates headers from content, so read headers until we find blank line. - int lineIndex = 1; - string line = null, nextLine = null; - while (lineIndex + 1 < lines.Length && !String.IsNullOrEmpty((line = lines[lineIndex++]))) - { - // If the following line starts with space or tab (or any whitespace), it is really part of this header but split for human readability. - // Combine these lines into a single comma separated style header for easier parsing. - while (lineIndex < lines.Length && !String.IsNullOrEmpty((nextLine = lines[lineIndex]))) - { - if (nextLine.Length > 0 && Char.IsWhiteSpace(nextLine[0])) - { - line += "," + nextLine.TrimStart(); - lineIndex++; - } - else - { - break; - } - } - - ParseHeader(line, headers, contentHeaders); - } - - return lineIndex; - } - - private List<string> ParseValues(string headerValue) - { - // This really should be better and match the HTTP 1.1 spec, - // but this should actually be good enough for SSDP implementations - // I think. - var values = new List<string>(); - - if (headerValue == "\"\"") - { - values.Add(string.Empty); - return values; - } - - var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters); - if (indexOfSeparator <= 0) - { - values.Add(headerValue); - } - else - { - var segments = headerValue.Split(SeparatorCharacters); - if (headerValue.Contains('"', StringComparison.Ordinal)) - { - for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++) - { - var segment = segments[segmentIndex]; - if (segment.Trim().StartsWith("\"", StringComparison.OrdinalIgnoreCase)) - { - segment = CombineQuotedSegments(segments, ref segmentIndex, segment); - } - - values.Add(segment); - } - } - else - { - values.AddRange(segments); - } - } - - return values; - } - - private string CombineQuotedSegments(string[] segments, ref int segmentIndex, string segment) - { - var trimmedSegment = segment.Trim(); - for (int index = segmentIndex; index < segments.Length; index++) - { - if (trimmedSegment == "\"\"" || - ( - trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase) - && !trimmedSegment.EndsWith("\"\"", StringComparison.OrdinalIgnoreCase) - && !trimmedSegment.EndsWith("\\\"", StringComparison.OrdinalIgnoreCase)) - ) - { - segmentIndex = index; - return trimmedSegment.Substring(1, trimmedSegment.Length - 2); - } - - if (index + 1 < segments.Length) - { - trimmedSegment += "," + segments[index + 1].TrimEnd(); - } - } - - segmentIndex = segments.Length; - if (trimmedSegment.StartsWith("\"", StringComparison.OrdinalIgnoreCase) && trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase)) - { - return trimmedSegment.Substring(1, trimmedSegment.Length - 2); - } - - return trimmedSegment; - } - } -} diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs deleted file mode 100644 index fab70eae2..000000000 --- a/RSSDP/HttpRequestParser.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Net.Http; -using Jellyfin.Extensions; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Parses a string into a <see cref="HttpRequestMessage"/> or throws an exception. - /// </summary> - public sealed class HttpRequestParser : HttpParserBase<HttpRequestMessage> - { - private readonly string[] ContentHeaderNames = new string[] - { - "Allow", "Content-Disposition", "Content-Encoding", "Content-Language", "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", "Expires", "Last-Modified" - }; - - /// <summary> - /// Parses the specified data into a <see cref="HttpRequestMessage"/> instance. - /// </summary> - /// <param name="data">A string containing the data to parse.</param> - /// <returns>A <see cref="HttpRequestMessage"/> instance containing the parsed data.</returns> - public override HttpRequestMessage Parse(string data) - { - HttpRequestMessage retVal = null; - - try - { - retVal = new HttpRequestMessage(); - - Parse(retVal, retVal.Headers, data); - - return retVal; - } - finally - { - retVal?.Dispose(); - } - } - - /// <summary> - /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>. - /// </summary> - /// <param name="data">The first line of the HTTP message to be parsed.</param> - /// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param> - protected override void ParseStatusLine(string data, HttpRequestMessage message) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - var parts = data.Split(' '); - if (parts.Length < 2) - { - throw new ArgumentException("Status line is invalid. Insufficient status parts.", nameof(data)); - } - - message.Method = new HttpMethod(parts[0].Trim()); - if (Uri.TryCreate(parts[1].Trim(), UriKind.RelativeOrAbsolute, out var requestUri)) - { - message.RequestUri = requestUri; - } - else - { - System.Diagnostics.Debug.WriteLine(parts[1]); - } - - if (parts.Length >= 3) - { - message.Version = ParseHttpVersion(parts[2].Trim()); - } - } - - /// <summary> - /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). - /// </summary> - /// <param name="headerName">A string containing the name of the header to return the type of.</param> - protected override bool IsContentHeader(string headerName) - { - return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase); - } - } -} diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs deleted file mode 100644 index c570c84cb..000000000 --- a/RSSDP/HttpResponseParser.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using Jellyfin.Extensions; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Parses a string into a <see cref="HttpResponseMessage"/> or throws an exception. - /// </summary> - public sealed class HttpResponseParser : HttpParserBase<HttpResponseMessage> - { - private readonly string[] ContentHeaderNames = new string[] - { - "Allow", "Content-Disposition", "Content-Encoding", "Content-Language", "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", "Expires", "Last-Modified" - }; - - /// <summary> - /// Parses the specified data into a <see cref="HttpResponseMessage"/> instance. - /// </summary> - /// <param name="data">A string containing the data to parse.</param> - /// <returns>A <see cref="HttpResponseMessage"/> instance containing the parsed data.</returns> - public override HttpResponseMessage Parse(string data) - { - HttpResponseMessage retVal = null; - try - { - retVal = new HttpResponseMessage(); - - Parse(retVal, retVal.Headers, data); - - return retVal; - } - catch - { - retVal?.Dispose(); - - throw; - } - } - - /// <summary> - /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). - /// </summary> - /// <param name="headerName">A string containing the name of the header to return the type of.</param> - /// <returns>A boolean, true if th specified header relates to HTTP content, otherwise false.</returns> - protected override bool IsContentHeader(string headerName) - { - return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>. - /// </summary> - /// <param name="data">The first line of the HTTP message to be parsed.</param> - /// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param> - protected override void ParseStatusLine(string data, HttpResponseMessage message) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - var parts = data.Split(' '); - if (parts.Length < 2) - { - throw new ArgumentException("data status line is invalid. Insufficient status parts.", nameof(data)); - } - - message.Version = ParseHttpVersion(parts[0].Trim()); - - 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)); - } - - message.StatusCode = (HttpStatusCode)statusCode; - - if (parts.Length >= 3) - { - message.ReasonPhrase = parts[2].Trim(); - } - } - } -} diff --git a/RSSDP/IEnumerableExtensions.cs b/RSSDP/IEnumerableExtensions.cs deleted file mode 100644 index 1f0daad3e..000000000 --- a/RSSDP/IEnumerableExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Rssdp.Infrastructure -{ - internal static class IEnumerableExtensions - { - public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (selector == null) - { - throw new ArgumentNullException(nameof(selector)); - } - - return !source.Any() ? source : - source.Concat( - source - .SelectMany(i => selector(i).EmptyIfNull()) - .SelectManyRecursive(selector) - ); - } - - public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T> source) - { - return source ?? Enumerable.Empty<T>(); - } - } -} diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs deleted file mode 100644 index 95b0a1c70..000000000 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Interface for a component that manages network communication (sending and receiving HTTPU messages) for the SSDP protocol. - /// </summary> - public interface ISsdpCommunicationsServer : IDisposable - { - /// <summary> - /// Raised when a HTTPU request message is received by a socket (unicast or multicast). - /// </summary> - event EventHandler<RequestReceivedEventArgs> RequestReceived; - - /// <summary> - /// Raised when an HTTPU response message is received by a socket (unicast or multicast). - /// </summary> - event EventHandler<ResponseReceivedEventArgs> ResponseReceived; - - /// <summary> - /// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications. - /// </summary> - void BeginListeningForMulticast(); - - /// <summary> - /// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications. - /// </summary> - void StopListeningForMulticast(); - - /// <summary> - /// Sends a message to a particular address (uni or multicast) and port. - /// </summary> - 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); - - /// <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. - /// </summary> - /// <remarks> - /// <para>If true, disposing an instance of a <see cref="SsdpDeviceLocator"/>or a <see cref="ISsdpDevicePublisher"/> will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server.</para> - /// </remarks> - bool IsShared { get; set; } - } -} diff --git a/RSSDP/ISsdpDeviceLocator.cs b/RSSDP/ISsdpDeviceLocator.cs deleted file mode 100644 index 4df166cd2..000000000 --- a/RSSDP/ISsdpDeviceLocator.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Interface for components that discover the existence of SSDP devices. - /// </summary> - /// <remarks> - /// <para>Discovering devices includes explicit search requests as well as listening for broadcast status notifications.</para> - /// </remarks> - /// <seealso cref="DiscoveredSsdpDevice"/> - /// <seealso cref="SsdpDevice"/> - /// <seealso cref="ISsdpDevicePublisher"/> - public interface ISsdpDeviceLocator - { - /// <summary> - /// Event raised when a device becomes available or is found by a search request. - /// </summary> - /// <seealso cref="NotificationFilter"/> - /// <seealso cref="DeviceUnavailable"/> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="StopListeningForNotifications"/> - event EventHandler<DeviceAvailableEventArgs> DeviceAvailable; - - /// <summary> - /// Event raised when a device explicitly notifies of shutdown or a device expires from the cache. - /// </summary> - /// <seeseealso cref="NotificationFilter"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="StopListeningForNotifications"/> - event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable; - - /// <summary> - /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="DeviceAvailable"/> or <see cref="DeviceUnavailable"/> events. - /// </summary> - /// <remarks> - /// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para> - /// <para>Example filters follow;</para> - /// <example>upnp:rootdevice</example> - /// <example>urn:schemas-upnp-org:device:WANDevice:1</example> - /// <example>"uuid:9F15356CC-95FA-572E-0E99-85B456BD3012"</example> - /// </remarks> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="DeviceUnavailable"/> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="StopListeningForNotifications"/> - string NotificationFilter - { - get; - set; - } - - /// <summary> - /// Asynchronously performs a search for all devices using the default search timeout, and returns an awaitable task that can be used to retrieve the results. - /// </summary> - /// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns> - System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(); - - /// <summary> - /// Performs a search for the specified search target (criteria) and default search timeout. - /// </summary> - /// <param name="searchTarget">The criteria for the search. Value can be; - /// <list type="table"> - /// <item><term>Root devices</term><description>upnp:rootdevice</description></item> - /// <item><term>Specific device by UUID</term><description>uuid:<device uuid></description></item> - /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item> - /// </list> - /// </param> - /// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns> - System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(string searchTarget); - - /// <summary> - /// Performs a search for the specified search target (criteria) and search timeout. - /// </summary> - /// <param name="searchTarget">The criteria for the search. Value can be; - /// <list type="table"> - /// <item><term>Root devices</term><description>upnp:rootdevice</description></item> - /// <item><term>Specific device by UUID</term><description>uuid:<device uuid></description></item> - /// <item><term>Device type</term><description>A device namespace and type in format of urn:<device namespace>:device:<device type>:<device version> i.e urn:schemas-upnp-org:device:Basic:1</description></item> - /// <item><term>Service type</term><description>A service namespace and type in format of urn:<service namespace>:service:<servicetype>:<service version> i.e urn:my-namespace:service:MyCustomService:1</description></item> - /// </list> - /// </param> - /// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 is recommended by the UPnP 1.1 specification. Specify TimeSpan.Zero to return only devices already in the cache.</param> - /// <remarks> - /// <para>By design RSSDP does not support 'publishing services' as it is intended for use with non-standard UPnP devices that don't publish UPnP style services. However, it is still possible to use RSSDP to search for devices implementing these services if you know the service type.</para> - /// </remarks> - /// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns> - System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(string searchTarget, TimeSpan searchWaitTime); - - /// <summary> - /// Performs a search for all devices using the specified search timeout. - /// </summary> - /// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 is recommended by the UPnP 1.1 specification. Specify TimeSpan.Zero to return only devices already in the cache.</param> - /// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns> - System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(TimeSpan searchWaitTime); - - /// <summary> - /// Starts listening for broadcast notifications of service availability. - /// </summary> - /// <remarks> - /// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para> - /// </remarks> - /// <seealso cref="StopListeningForNotifications"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="DeviceUnavailable"/> - /// <seealso cref="NotificationFilter"/> - void StartListeningForNotifications(); - - /// <summary> - /// Stops listening for broadcast notifications of service availability. - /// </summary> - /// <remarks> - /// <para>Does nothing if this instance is not already listening for notifications.</para> - /// </remarks> - /// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="DeviceUnavailable"/> - /// <seealso cref="NotificationFilter"/> - void StopListeningForNotifications(); - } -} diff --git a/RSSDP/ISsdpDevicePublisher.cs b/RSSDP/ISsdpDevicePublisher.cs deleted file mode 100644 index 96c15443d..000000000 --- a/RSSDP/ISsdpDevicePublisher.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Interface for components that publish the existence of SSDP devices. - /// </summary> - /// <remarks> - /// <para>Publishing a device includes sending notifications (alive and byebye) as well as responding to search requests when appropriate.</para> - /// </remarks> - /// <seealso cref="SsdpRootDevice"/> - /// <seealso cref="ISsdpDeviceLocator"/> - public interface ISsdpDevicePublisher - { - /// <summary> - /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients. - /// </summary> - /// <param name="device">The <see cref="SsdpRootDevice"/> instance to add.</param> - /// <returns>An awaitable <see cref="Task"/>.</returns> - void AddDevice(SsdpRootDevice device); - - /// <summary> - /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable. - /// </summary> - /// <param name="device">The <see cref="SsdpRootDevice"/> instance to add.</param> - /// <returns>An awaitable <see cref="Task"/>.</returns> - Task RemoveDevice(SsdpRootDevice device); - - /// <summary> - /// Returns a read only list of devices being published by this instance. - /// </summary> - /// <seealso cref="SsdpDevice"/> - System.Collections.Generic.IEnumerable<SsdpRootDevice> Devices { get; } - } -} diff --git a/RSSDP/LICENSE b/RSSDP/LICENSE deleted file mode 100644 index aabeb93af..000000000 --- a/RSSDP/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -RSSDP - -Copyright (c) 2015 Troy Willmot -Copyright (c) 2015-2018 Luke Pulverenti diff --git a/RSSDP/Properties/AssemblyInfo.cs b/RSSDP/Properties/AssemblyInfo.cs deleted file mode 100644 index 55f7b6a83..000000000 --- a/RSSDP/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,24 +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("RSSDP")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2015 Troy Willmot. Code released under the MIT license. 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)] - -[assembly: AssemblyVersion("1.0.3.0")] -[assembly: AssemblyFileVersion("2019.1.20.3")] diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj deleted file mode 100644 index 3f24de4e6..000000000 --- a/RSSDP/RSSDP.csproj +++ /dev/null @@ -1,21 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> - <PropertyGroup> - <ProjectGuid>{21002819-C39A-4D3E-BE83-2A276A77FB1F}</ProjectGuid> - </PropertyGroup> - - <ItemGroup> - <ProjectReference Include="..\Jellyfin.Networking\Jellyfin.Networking.csproj" /> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> - </ItemGroup> - - <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> - <GenerateAssemblyInfo>false</GenerateAssemblyInfo> - <AnalysisMode>AllDisabledByDefault</AnalysisMode> - <Nullable>disable</Nullable> - <NoWarn>CA2016</NoWarn> - </PropertyGroup> - -</Project> diff --git a/RSSDP/RequestReceivedEventArgs.cs b/RSSDP/RequestReceivedEventArgs.cs deleted file mode 100644 index b8b2249e4..000000000 --- a/RSSDP/RequestReceivedEventArgs.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Provides arguments for the <see cref="ISsdpCommunicationsServer.RequestReceived"/> event. - /// </summary> - public sealed class RequestReceivedEventArgs : EventArgs - { - private readonly HttpRequestMessage _Message; - - private readonly IPEndPoint _ReceivedFrom; - - public IPAddress LocalIPAddress { get; private set; } - - /// <summary> - /// Full constructor. - /// </summary> - public RequestReceivedEventArgs(HttpRequestMessage message, IPEndPoint receivedFrom, IPAddress localIPAddress) - { - _Message = message; - _ReceivedFrom = receivedFrom; - LocalIPAddress = localIPAddress; - } - - /// <summary> - /// The <see cref="HttpRequestMessage"/> that was received. - /// </summary> - public HttpRequestMessage Message - { - get { return _Message; } - } - - /// <summary> - /// The <see cref="IPEndPoint"/> the request came from. - /// </summary> - public IPEndPoint ReceivedFrom - { - get { return _ReceivedFrom; } - } - } -} diff --git a/RSSDP/ResponseReceivedEventArgs.cs b/RSSDP/ResponseReceivedEventArgs.cs deleted file mode 100644 index e87ba1452..000000000 --- a/RSSDP/ResponseReceivedEventArgs.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Provides arguments for the <see cref="ISsdpCommunicationsServer.ResponseReceived"/> event. - /// </summary> - public sealed class ResponseReceivedEventArgs : EventArgs - { - public IPAddress LocalIPAddress { get; set; } - - private readonly HttpResponseMessage _Message; - - private readonly IPEndPoint _ReceivedFrom; - - /// <summary> - /// Full constructor. - /// </summary> - public ResponseReceivedEventArgs(HttpResponseMessage message, IPEndPoint receivedFrom) - { - _Message = message; - _ReceivedFrom = receivedFrom; - } - - /// <summary> - /// The <see cref="HttpResponseMessage"/> that was received. - /// </summary> - public HttpResponseMessage Message - { - get { return _Message; } - } - - /// <summary> - /// The <see cref="IPEndPoint"/> the response came from. - /// </summary> - public IPEndPoint ReceivedFrom - { - get { return _ReceivedFrom; } - } - } -} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs deleted file mode 100644 index 42563e2ed..000000000 --- a/RSSDP/SsdpCommunicationsServer.cs +++ /dev/null @@ -1,523 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Provides the platform independent logic for publishing device existence and responding to search requests. - /// </summary> - public sealed class SsdpCommunicationsServer : DisposableManagedObjectBase, ISsdpCommunicationsServer - { - /* We could technically use one socket listening on port 1900 for everything. - * This should get both multicast (notifications) and unicast (search response) messages, however - * this often doesn't work under Windows because the MS SSDP service is running. If that service - * is running then it will steal the unicast messages and we will never see search responses. - * 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 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(); - private List<Socket> _MulticastListenSockets; - - private object _SendSocketSynchroniser = new(); - private List<Socket> _sendSockets; - - private HttpRequestParser _RequestParser; - private HttpResponseParser _ResponseParser; - private readonly ILogger _logger; - private ISocketFactory _SocketFactory; - private readonly INetworkManager _networkManager; - - private int _LocalPort; - private int _MulticastTtl; - - private bool _IsShared; - - /// <summary> - /// Raised when a HTTPU request message is received by a socket (unicast or multicast). - /// </summary> - public event EventHandler<RequestReceivedEventArgs> RequestReceived; - - /// <summary> - /// Raised when an HTTPU response message is received by a socket (unicast or multicast). - /// </summary> - public event EventHandler<ResponseReceivedEventArgs> ResponseReceived; - - /// <summary> - /// Minimum constructor. - /// </summary> - /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> - public SsdpCommunicationsServer( - ISocketFactory socketFactory, - INetworkManager networkManager, - ILogger logger) - : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger) - { - - } - - /// <summary> - /// Full constructor. - /// </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) - { - if (socketFactory is null) - { - throw new ArgumentNullException(nameof(socketFactory)); - } - - if (multicastTimeToLive <= 0) - { - throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero."); - } - - _BroadcastListenSocketSynchroniser = new(); - _SendSocketSynchroniser = new(); - - _LocalPort = localPort; - _SocketFactory = socketFactory; - - _RequestParser = new(); - _ResponseParser = new(); - - _MulticastTtl = multicastTimeToLive; - _networkManager = networkManager; - _logger = logger; - } - - /// <summary> - /// 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 BeginListeningForMulticast() - { - ThrowIfDisposed(); - - lock (_BroadcastListenSocketSynchroniser) - { - if (_MulticastListenSockets is null) - { - try - { - _MulticastListenSockets = CreateMulticastSocketsAndListen(); - } - catch (SocketException ex) - { - _logger.LogError("Failed to bind to multicast address: {Message}. DLNA will be unavailable", ex.Message); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in BeginListeningForMulticast"); - } - } - } - } - - /// <summary> - /// 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 StopListeningForMulticast() - { - lock (_BroadcastListenSocketSynchroniser) - { - if (_MulticastListenSockets is not null) - { - _logger.LogInformation("{0} disposing _BroadcastListenSocket", GetType().Name); - foreach (var socket in _MulticastListenSockets) - { - socket.Dispose(); - } - - _MulticastListenSockets = null; - } - } - } - - /// <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) - { - if (messageData is null) - { - throw new ArgumentNullException(nameof(messageData)); - } - - ThrowIfDisposed(); - - var sockets = GetSendSockets(fromlocalIPAddress, destination); - - if (sockets.Count == 0) - { - return; - } - - // SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP. - for (var i = 0; i < SsdpConstants.UdpResendCount; i++) - { - var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken)).ToArray(); - await Task.WhenAll(tasks).ConfigureAwait(false); - - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - } - - private async Task SendFromSocket(Socket socket, byte[] messageData, IPEndPoint destination, CancellationToken cancellationToken) - { - try - { - await socket.SendToAsync(messageData, destination, cancellationToken).ConfigureAwait(false); - } - catch (ObjectDisposedException) - { - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - var localIP = ((IPEndPoint)socket.LocalEndPoint).Address; - _logger.LogError(ex, "Error sending socket message from {0} to {1}", localIP.ToString(), destination.ToString()); - } - } - - private List<Socket> GetSendSockets(IPAddress fromlocalIPAddress, IPEndPoint destination) - { - EnsureSendSocketCreated(); - - lock (_SendSocketSynchroniser) - { - 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(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(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any) - || ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Loopback)); - } - } - else if (fromlocalIPAddress.AddressFamily == AddressFamily.InterNetworkV6) - { - 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(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any) - || ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Loopback)); - } - } - - return sockets.ToList(); - } - } - - public Task SendMulticastMessage(string message, IPAddress fromlocalIPAddress, CancellationToken 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) - { - if (message is null) - { - throw new ArgumentNullException(nameof(message)); - } - - byte[] messageData = Encoding.UTF8.GetBytes(message); - - ThrowIfDisposed(); - - cancellationToken.ThrowIfCancellationRequested(); - - EnsureSendSocketCreated(); - - // SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP. - for (var i = 0; i < sendCount; i++) - { - await SendMessageIfSocketNotDisposed( - messageData, - new IPEndPoint( - IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), - SsdpConstants.MulticastPort), - fromlocalIPAddress, - cancellationToken).ConfigureAwait(false); - - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - } - - /// <summary> - /// Stops listening for search responses on the local, unicast socket. - /// </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 StopListeningForResponses() - { - lock (_SendSocketSynchroniser) - { - if (_sendSockets is not null) - { - var sockets = _sendSockets.ToList(); - _sendSockets = null; - - _logger.LogInformation("{0} Disposing {1} sendSockets", GetType().Name, sockets.Count); - - foreach (var socket in sockets) - { - var socketAddress = ((IPEndPoint)socket.LocalEndPoint).Address; - _logger.LogInformation("{0} disposing sendSocket from {1}", GetType().Name, socketAddress); - socket.Dispose(); - } - } - } - } - - /// <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. - /// </summary> - /// <remarks> - /// <para>If true, disposing an instance of a <see cref="SsdpDeviceLocator"/>or a <see cref="ISsdpDevicePublisher"/> will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server.</para> - /// </remarks> - public bool IsShared - { - get { return _IsShared; } - - set { _IsShared = value; } - } - - /// <summary> - /// Stops listening for requests, disposes this instance and all internal resources. - /// </summary> - /// <param name="disposing"></param> - protected override void Dispose(bool disposing) - { - if (disposing) - { - StopListeningForMulticast(); - - StopListeningForResponses(); - } - } - - 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)) - .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); - return Task.WhenAll(tasks); - } - - return Task.CompletedTask; - } - - private List<Socket> CreateMulticastSocketsAndListen() - { - var sockets = new List<Socket>(); - var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress); - - // 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) - { - 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; - } - - private List<Socket> CreateSendSockets() - { - var sockets = new List<Socket>(); - - // 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 - { - 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); - } - } - - return sockets; - } - - private async Task ListenToSocketInternal(Socket socket) - { - var cancelled = false; - var receiveBuffer = new byte[8192]; - - while (!cancelled && !IsDisposed) - { - try - { - var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false); - - if (result.ReceivedBytes > 0) - { - var remoteEndpoint = (IPEndPoint)result.RemoteEndPoint; - var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface); - - ProcessMessage( - Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), - remoteEndpoint, - localEndpointAdapter.Address); - } - } - catch (ObjectDisposedException) - { - cancelled = true; - } - catch (TaskCanceledException) - { - cancelled = true; - } - } - } - - private void EnsureSendSocketCreated() - { - if (_sendSockets is null) - { - lock (_SendSocketSynchroniser) - { - _sendSockets ??= CreateSendSockets(); - } - } - } - - 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); - if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase)) - { - HttpResponseMessage responseMessage = null; - try - { - responseMessage = _ResponseParser.Parse(data); - } - catch (ArgumentException) - { - // Ignore invalid packets. - } - - if (responseMessage is not null) - { - OnResponseReceived(responseMessage, endPoint, receivedOnlocalIPAddress); - } - } - else - { - HttpRequestMessage requestMessage = null; - try - { - requestMessage = _RequestParser.Parse(data); - } - catch (ArgumentException) - { - // Ignore invalid packets. - } - - if (requestMessage is not null) - { - OnRequestReceived(requestMessage, endPoint, 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. - // Section 4.2 - http://tools.ietf.org/html/draft-cai-ssdp-v1-03#page-11 - if (data.RequestUri.ToString() != "*") - { - return; - } - - var handlers = RequestReceived; - handlers?.Invoke(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); - } - - private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) - { - var handlers = ResponseReceived; - handlers?.Invoke(this, new ResponseReceivedEventArgs(data, endPoint) - { - LocalIPAddress = localIPAddress - }); - } - } -} diff --git a/RSSDP/SsdpConstants.cs b/RSSDP/SsdpConstants.cs deleted file mode 100644 index 442f2b8f8..000000000 --- a/RSSDP/SsdpConstants.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Provides constants for common values related to the SSDP protocols. - /// </summary> - public static class SsdpConstants - { - - /// <summary> - /// Multicast IP Address used for SSDP multicast messages. Values is 239.255.255.250. - /// </summary> - public const string MulticastLocalAdminAddress = "239.255.255.250"; - /// <summary> - /// The UDP port used for SSDP multicast messages. Values is 1900. - /// </summary> - public const int MulticastPort = 1900; - /// <summary> - /// The default multicase TTL for SSDP multicast messages. Value is 4. - /// </summary> - public const int SsdpDefaultMulticastTimeToLive = 4; - - internal const string MSearchMethod = "M-SEARCH"; - - internal const string SsdpDiscoverMessage = "ssdp:discover"; - internal const string SsdpDiscoverAllSTHeader = "ssdp:all"; - - 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> - public const int DefaultUdpSocketBufferSize = 8192; - /// <summary> - /// The maximum possible buffer size for a UDP message. Value is 65507 (bytes). - /// </summary> - public const int MaxUdpSocketBufferSize = 65507; // Max possible UDP packet size on IPv4 without using 'jumbograms'. - - /// <summary> - /// Namespace/prefix for UPnP device types. Values is schemas-upnp-org. - /// </summary> - public const string UpnpDeviceTypeNamespace = "schemas-upnp-org"; - /// <summary> - /// UPnP Root Device type. Value is upnp:rootdevice. - /// </summary> - public const string UpnpDeviceTypeRootDevice = "upnp:rootdevice"; - /// <summary> - /// The value is used by Windows Explorer for device searches instead of the UPNPDeviceTypeRootDevice constant. - /// Not sure why (different spec, bug, alternate protocol etc). Used to enable Windows Explorer support. - /// </summary> - public const string PnpDeviceTypeRootDevice = "pnp:rootdevice"; - /// <summary> - /// UPnP Basic Device type. Value is Basic. - /// </summary> - public const string UpnpDeviceTypeBasicDevice = "Basic"; - - internal const string SsdpKeepAliveNotification = "ssdp:alive"; - internal const string SsdpByeByeNotification = "ssdp:byebye"; - - internal const int UdpResendCount = 3; - } -} diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs deleted file mode 100644 index 569d733ea..000000000 --- a/RSSDP/SsdpDevice.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using Rssdp.Infrastructure; - -namespace Rssdp -{ - /// <summary> - /// Base class representing the common details of a (root or embedded) device, either to be published or that has been located. - /// </summary> - /// <remarks> - /// <para>Do not derive new types directly from this class. New device classes should derive from either <see cref="SsdpRootDevice"/> or <see cref="SsdpEmbeddedDevice"/>.</para> - /// </remarks> - /// <seealso cref="SsdpRootDevice"/> - /// <seealso cref="SsdpEmbeddedDevice"/> - public abstract class SsdpDevice - { - private string _Udn; - private string _DeviceType; - private string _DeviceTypeNamespace; - private int _DeviceVersion; - - private IList<SsdpDevice> _Devices; - - /// <summary> - /// Raised when a new child device is added. - /// </summary> - /// <seealso cref="AddDevice"/> - /// <seealso cref="DeviceAdded"/> - public event EventHandler<DeviceEventArgs> DeviceAdded; - - /// <summary> - /// Raised when a child device is removed. - /// </summary> - /// <seealso cref="RemoveDevice"/> - /// <seealso cref="DeviceRemoved"/> - public event EventHandler<DeviceEventArgs> DeviceRemoved; - - /// <summary> - /// Derived type constructor, allows constructing a device with no parent. Should only be used from derived types that are or inherit from <see cref="SsdpRootDevice"/>. - /// </summary> - protected SsdpDevice() - { - _DeviceTypeNamespace = SsdpConstants.UpnpDeviceTypeNamespace; - _DeviceType = SsdpConstants.UpnpDeviceTypeBasicDevice; - _DeviceVersion = 1; - - _Devices = new List<SsdpDevice>(); - this.Devices = new ReadOnlyCollection<SsdpDevice>(_Devices); - } - - public SsdpRootDevice ToRootDevice() - { - var device = this; - - var rootDevice = device as SsdpRootDevice; - if (rootDevice == null) - { - rootDevice = ((SsdpEmbeddedDevice)device).RootDevice; - } - - return rootDevice; - } - - /// <summary> - /// Sets or returns the core device type (not including namespace, version etc.). Required. - /// </summary> - /// <remarks><para>Defaults to the UPnP basic device type.</para></remarks> - /// <seealso cref="DeviceTypeNamespace"/> - /// <seealso cref="DeviceVersion"/> - /// <seealso cref="FullDeviceType"/> - public string DeviceType - { - get - { - return _DeviceType; - } - - set - { - _DeviceType = value; - } - } - - public string DeviceClass { get; set; } - - /// <summary> - /// Sets or returns the namespace for the <see cref="DeviceType"/> of this device. Optional, but defaults to UPnP schema so should be changed if <see cref="DeviceType"/> is not a UPnP device type. - /// </summary> - /// <remarks><para>Defaults to the UPnP standard namespace.</para></remarks> - /// <seealso cref="DeviceType"/> - /// <seealso cref="DeviceVersion"/> - /// <seealso cref="FullDeviceType"/> - public string DeviceTypeNamespace - { - get - { - return _DeviceTypeNamespace; - } - - set - { - _DeviceTypeNamespace = value; - } - } - - /// <summary> - /// Sets or returns the version of the device type. Optional, defaults to 1. - /// </summary> - /// <remarks><para>Defaults to a value of 1.</para></remarks> - /// <seealso cref="DeviceType"/> - /// <seealso cref="DeviceTypeNamespace"/> - /// <seealso cref="FullDeviceType"/> - public int DeviceVersion - { - get - { - return _DeviceVersion; - } - - set - { - _DeviceVersion = value; - } - } - - /// <summary> - /// Returns the full device type string. - /// </summary> - /// <remarks> - /// <para>The format used is urn:<see cref="DeviceTypeNamespace"/>:device:<see cref="DeviceType"/>:<see cref="DeviceVersion"/></para> - /// </remarks> - public string FullDeviceType - { - get - { - return String.Format( - CultureInfo.InvariantCulture, - "urn:{0}:{3}:{1}:{2}", - this.DeviceTypeNamespace ?? String.Empty, - this.DeviceType ?? String.Empty, - this.DeviceVersion, - this.DeviceClass ?? "device"); - } - } - - /// <summary> - /// Sets or returns the universally unique identifier for this device (without the uuid: prefix). Required. - /// </summary> - /// <remarks> - /// <para>Must be the same over time for a specific device instance (i.e. must survive reboots).</para> - /// <para>For UPnP 1.0 this can be any unique string. For UPnP 1.1 this should be a 128 bit number formatted in a specific way, preferably generated using the time and MAC based algorithm. See section 1.1.4 of http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf for details.</para> - /// <para>Technically this library implements UPnP 1.0, so any value is allowed, but we advise using UPnP 1.1 compatible values for good behaviour and forward compatibility with future versions.</para> - /// </remarks> - public string Uuid { get; set; } - - /// <summary> - /// Returns (or sets*) a unique device name for this device. Optional, not recommended to be explicitly set. - /// </summary> - /// <remarks> - /// <para>* In general you should not explicitly set this property. If it is not set (or set to null/empty string) the property will return a UDN value that is correct as per the UPnP specification, based on the other device properties.</para> - /// <para>The setter is provided to allow for devices that do not correctly follow the specification (when we discover them), rather than to intentionally deviate from the specification.</para> - /// <para>If a value is explicitly set, it is used verbatim, and so any prefix (such as uuid:) must be provided in the value.</para> - /// </remarks> - public string Udn - { - get - { - if (String.IsNullOrEmpty(_Udn) && !String.IsNullOrEmpty(this.Uuid)) - { - return "uuid:" + this.Uuid; - } - - return _Udn; - } - - set - { - _Udn = value; - } - } - - /// <summary> - /// Sets or returns a friendly/display name for this device on the network. Something the user can identify the device/instance by, i.e Lounge Main Light. Required. - /// </summary> - /// <remarks><para>A short description for the end user. </para></remarks> - public string FriendlyName { get; set; } - - /// <summary> - /// Sets or returns the name of the manufacturer of this device. Required. - /// </summary> - public string Manufacturer { get; set; } - - /// <summary> - /// Sets or returns a URL to the manufacturers web site. Optional. - /// </summary> - public Uri ManufacturerUrl { get; set; } - - /// <summary> - /// Sets or returns a description of this device model. Recommended. - /// </summary> - /// <remarks><para>A long description for the end user.</para></remarks> - public string ModelDescription { get; set; } - - /// <summary> - /// Sets or returns the name of this model. Required. - /// </summary> - public string ModelName { get; set; } - - /// <summary> - /// Sets or returns the number of this model. Recommended. - /// </summary> - public string ModelNumber { get; set; } - - /// <summary> - /// Sets or returns a URL to a web page with details of this device model. Optional. - /// </summary> - /// <remarks> - /// <para>Optional. May be relative to base URL.</para> - /// </remarks> - public Uri ModelUrl { get; set; } - - /// <summary> - /// Sets or returns the serial number for this device. Recommended. - /// </summary> - public string SerialNumber { get; set; } - - /// <summary> - /// Sets or returns the universal product code of the device, if any. Optional. - /// </summary> - /// <remarks> - /// <para>If not blank, must be exactly 12 numeric digits.</para> - /// </remarks> - public string Upc { get; set; } - - /// <summary> - /// Sets or returns the URL to a web page that can be used to configure/manager/use the device. Recommended. - /// </summary> - /// <remarks> - /// <para>May be relative to base URL. </para> - /// </remarks> - public Uri PresentationUrl { get; set; } - - /// <summary> - /// Returns a read-only enumerable set of <see cref="SsdpDevice"/> objects representing children of this device. Child devices are optional. - /// </summary> - /// <seealso cref="AddDevice"/> - /// <seealso cref="RemoveDevice"/> - public IList<SsdpDevice> Devices - { - get; - private set; - } - - /// <summary> - /// Adds a child device to the <see cref="Devices"/> collection. - /// </summary> - /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance to add.</param> - /// <remarks> - /// <para>If the device is already a member of the <see cref="Devices"/> collection, this method does nothing.</para> - /// <para>Also sets the <see cref="SsdpEmbeddedDevice.RootDevice"/> property of the added device and all descendant devices to the relevant <see cref="SsdpRootDevice"/> instance.</para> - /// </remarks> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - /// <exception cref="InvalidOperationException">Thrown if the <paramref name="device"/> is already associated with a different <see cref="SsdpRootDevice"/> instance than used in this tree. Can occur if you try to add the same device instance to more than one tree. Also thrown if you try to add a device to itself.</exception> - /// <seealso cref="DeviceAdded"/> - public void AddDevice(SsdpEmbeddedDevice device) - { - if (device == null) - { - throw new ArgumentNullException(nameof(device)); - } - - if (device.RootDevice != null && device.RootDevice != this.ToRootDevice()) - { - throw new InvalidOperationException("This device is already associated with a different root device (has been added as a child in another branch)."); - } - - if (device == this) - { - throw new InvalidOperationException("Can't add device to itself."); - } - - bool wasAdded = false; - lock (_Devices) - { - device.RootDevice = this.ToRootDevice(); - _Devices.Add(device); - wasAdded = true; - } - - if (wasAdded) - { - OnDeviceAdded(device); - } - } - - /// <summary> - /// Removes a child device from the <see cref="Devices"/> collection. - /// </summary> - /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance to remove.</param> - /// <remarks> - /// <para>If the device is not a member of the <see cref="Devices"/> collection, this method does nothing.</para> - /// <para>Also sets the <see cref="SsdpEmbeddedDevice.RootDevice"/> property to null for the removed device and all descendant devices.</para> - /// </remarks> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - /// <seealso cref="DeviceRemoved"/> - public void RemoveDevice(SsdpEmbeddedDevice device) - { - if (device == null) - { - throw new ArgumentNullException(nameof(device)); - } - - bool wasRemoved = false; - lock (_Devices) - { - wasRemoved = _Devices.Remove(device); - if (wasRemoved) - { - device.RootDevice = null; - } - } - - if (wasRemoved) - { - OnDeviceRemoved(device); - } - } - - /// <summary> - /// Raises the <see cref="DeviceAdded"/> event. - /// </summary> - /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance added to the <see cref="Devices"/> collection.</param> - /// <seealso cref="AddDevice"/> - /// <seealso cref="DeviceAdded"/> - protected virtual void OnDeviceAdded(SsdpEmbeddedDevice device) - { - var handlers = this.DeviceAdded; - handlers?.Invoke(this, new DeviceEventArgs(device)); - } - - /// <summary> - /// Raises the <see cref="DeviceRemoved"/> event. - /// </summary> - /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance removed from the <see cref="Devices"/> collection.</param> - /// <seealso cref="RemoveDevice"/> - /// <see cref="DeviceRemoved"/> - protected virtual void OnDeviceRemoved(SsdpEmbeddedDevice device) - { - var handlers = this.DeviceRemoved; - handlers?.Invoke(this, new DeviceEventArgs(device)); - } - } -} diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs deleted file mode 100644 index d6fad4b9d..000000000 --- a/RSSDP/SsdpDeviceLocator.cs +++ /dev/null @@ -1,626 +0,0 @@ -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> - /// Allows you to search the network for a particular device, device types, or UPnP service types. Also listenings for broadcast notifications of device availability and raises events to indicate changes in status. - /// </summary> - public class SsdpDeviceLocator : DisposableManagedObjectBase - { - private List<DiscoveredSsdpDevice> _Devices; - private ISsdpCommunicationsServer _CommunicationsServer; - - private Timer _BroadcastTimer; - private object _timerLock = new(); - - 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, - string osName, - string osVersion) - { - ArgumentNullException.ThrowIfNull(communicationsServer); - ArgumentNullException.ThrowIfNullOrEmpty(osName); - ArgumentNullException.ThrowIfNullOrEmpty(osVersion); - - _OSName = osName; - _OSVersion = osVersion; - _CommunicationsServer = communicationsServer; - _CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived; - - _Devices = new List<DiscoveredSsdpDevice>(); - } - - /// <summary> - /// Raised for when - /// <list type="bullet"> - /// <item>An 'alive' notification is received that a device, regardless of whether or not that device is not already in the cache or has previously raised this event.</item> - /// <item>For each item found during a device <see cref="SearchAsync(System.Threading.CancellationToken)"/> (cached or not), allowing clients to respond to found devices before the entire search is complete.</item> - /// <item>Only if the notification type matches the <see cref="NotificationFilter"/> property. By default the filter is null, meaning all notifications raise events (regardless of ant </item> - /// </list> - /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para> - /// </summary> - /// <seealso cref="NotificationFilter"/> - /// <seealso cref="DeviceUnavailable"/> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="StopListeningForNotifications"/> - public event EventHandler<DeviceAvailableEventArgs> DeviceAvailable; - - /// <summary> - /// Raised when a notification is received that indicates a device has shutdown or otherwise become unavailable. - /// </summary> - /// <remarks> - /// <para>Devices *should* broadcast these types of notifications, but not all devices do and sometimes (in the event of power loss for example) it might not be possible for a device to do so. You should also implement error handling when trying to contact a device, even if RSSDP is reporting that device as available.</para> - /// <para>This event is only raised if the notification type matches the <see cref="NotificationFilter"/> property. A null or empty string for the <see cref="NotificationFilter"/> will be treated as no filter and raise the event for all notifications.</para> - /// <para>The <see cref="DeviceUnavailableEventArgs.DiscoveredDevice"/> property may contain either a fully complete <see cref="DiscoveredSsdpDevice"/> instance, or one containing just a USN and NotificationType property. Full information is available if the device was previously discovered and cached, but only partial information if a byebye notification was received for a previously unseen or expired device.</para> - /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para> - /// </remarks> - /// <seealso cref="NotificationFilter"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="StopListeningForNotifications"/> - public event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable; - - public void RestartBroadcastTimer(TimeSpan dueTime, TimeSpan period) - { - lock (_timerLock) - { - if (_BroadcastTimer is null) - { - _BroadcastTimer = new Timer(OnBroadcastTimerCallback, null, dueTime, period); - } - else - { - _BroadcastTimer.Change(dueTime, period); - } - } - } - - public void DisposeBroadcastTimer() - { - lock (_timerLock) - { - if (_BroadcastTimer is not null) - { - _BroadcastTimer.Dispose(); - _BroadcastTimer = null; - } - } - } - - private async void OnBroadcastTimerCallback(object state) - { - if (IsDisposed) - { - return; - } - - StartListeningForNotifications(); - RemoveExpiredDevicesFromCache(); - - try - { - await SearchAsync(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception) - { - } - } - - /// <summary> - /// Performs a search for all devices using the default search timeout. - /// </summary> - private Task SearchAsync(CancellationToken cancellationToken) - { - return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken); - } - - /// <summary> - /// Performs a search for the specified search target (criteria) and default search timeout. - /// </summary> - /// <param name="searchTarget">The criteria for the search. Value can be; - /// <list type="table"> - /// <item><term>Root devices</term><description>upnp:rootdevice</description></item> - /// <item><term>Specific device by UUID</term><description>uuid:<device uuid></description></item> - /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item> - /// </list> - /// </param> - private Task SearchAsync(string searchTarget) - { - return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None); - } - - /// <summary> - /// Performs a search for all devices using the specified search timeout. - /// </summary> - /// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 seconds is recommended by the UPnP 1.1 specification, this method requires the value be greater 1 second if it is not zero. Specify TimeSpan.Zero to return only devices already in the cache.</param> - private Task SearchAsync(TimeSpan searchWaitTime) - { - return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, searchWaitTime, CancellationToken.None); - } - - private Task SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken) - { - if (searchTarget is null) - { - throw new ArgumentNullException(nameof(searchTarget)); - } - - if (searchTarget.Length == 0) - { - throw new ArgumentException("searchTarget cannot be an empty string.", nameof(searchTarget)); - } - - if (searchWaitTime.TotalSeconds < 0) - { - throw new ArgumentException("searchWaitTime must be a positive time."); - } - - if (searchWaitTime.TotalSeconds > 0 && searchWaitTime.TotalSeconds <= 1) - { - throw new ArgumentException("searchWaitTime must be zero (if you are not using the result and relying entirely in the events), or greater than one second."); - } - - ThrowIfDisposed(); - - return BroadcastDiscoverMessage(searchTarget, SearchTimeToMXValue(searchWaitTime), cancellationToken); - } - - /// <summary> - /// Starts listening for broadcast notifications of service availability. - /// </summary> - /// <remarks> - /// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para> - /// </remarks> - /// <seealso cref="StopListeningForNotifications"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="DeviceUnavailable"/> - /// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> ty is true.</exception> - public void StartListeningForNotifications() - { - _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; - _CommunicationsServer.RequestReceived += CommsServer_RequestReceived; - _CommunicationsServer.BeginListeningForMulticast(); - } - - /// <summary> - /// Stops listening for broadcast notifications of service availability. - /// </summary> - /// <remarks> - /// <para>Does nothing if this instance is not already listening for notifications.</para> - /// </remarks> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="DeviceUnavailable"/> - /// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception> - public void StopListeningForNotifications() - { - ThrowIfDisposed(); - - _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; - } - - /// <summary> - /// Raises the <see cref="DeviceAvailable"/> event. - /// </summary> - /// <seealso cref="DeviceAvailable"/> - protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) - { - if (IsDisposed) - { - return; - } - - var handlers = DeviceAvailable; - handlers?.Invoke(this, new DeviceAvailableEventArgs(device, isNewDevice) - { - RemoteIPAddress = IPAddress - }); - } - - /// <summary> - /// Raises the <see cref="DeviceUnavailable"/> event. - /// </summary> - /// <param name="device">A <see cref="DiscoveredSsdpDevice"/> representing the device that is no longer available.</param> - /// <param name="expired">True if the device expired from the cache without being renewed, otherwise false to indicate the device explicitly notified us it was being shutdown.</param> - /// <seealso cref="DeviceUnavailable"/> - protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) - { - if (IsDisposed) - { - return; - } - - var handlers = DeviceUnavailable; - handlers?.Invoke(this, new DeviceUnavailableEventArgs(device, expired)); - } - - /// <summary> - /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="ISsdpDeviceLocator.DeviceAvailable"/> or <see cref="ISsdpDeviceLocator.DeviceUnavailable"/> events. - /// </summary> - /// <remarks> - /// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para> - /// <para>If the value is null or empty string then, all notifications are reported.</para> - /// <para>Example filters follow;</para> - /// <example>upnp:rootdevice</example> - /// <example>urn:schemas-upnp-org:device:WANDevice:1</example> - /// <example>uuid:9F15356CC-95FA-572E-0E99-85B456BD3012</example> - /// </remarks> - /// <seealso cref="ISsdpDeviceLocator.DeviceAvailable"/> - /// <seealso cref="ISsdpDeviceLocator.DeviceUnavailable"/> - /// <seealso cref="ISsdpDeviceLocator.StartListeningForNotifications"/> - /// <seealso cref="ISsdpDeviceLocator.StopListeningForNotifications"/> - public string NotificationFilter - { - get; - set; - } - - /// <summary> - /// Disposes this object and all internal resources. Stops listening for all network messages. - /// </summary> - /// <param name="disposing">True if managed resources should be disposed, or false is only unmanaged resources should be cleaned up.</param> - protected override void Dispose(bool disposing) - { - if (disposing) - { - DisposeBroadcastTimer(); - - var commsServer = _CommunicationsServer; - _CommunicationsServer = null; - if (commsServer is not null) - { - commsServer.ResponseReceived -= CommsServer_ResponseReceived; - commsServer.RequestReceived -= CommsServer_RequestReceived; - } - } - } - - private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IPAddress) - { - bool isNewDevice = false; - lock (_Devices) - { - var existingDevice = FindExistingDeviceNotification(_Devices, device.NotificationType, device.Usn); - if (existingDevice is null) - { - _Devices.Add(device); - isNewDevice = true; - } - else - { - _Devices.Remove(existingDevice); - _Devices.Add(device); - } - } - - DeviceFound(device, isNewDevice, IPAddress); - } - - private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) - { - if (!NotificationTypeMatchesFilter(device)) - { - return; - } - - OnDeviceAvailable(device, isNewDevice, IPAddress); - } - - private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device) - { - return String.IsNullOrEmpty(this.NotificationFilter) - || this.NotificationFilter == SsdpConstants.SsdpDiscoverAllSTHeader - || device.NotificationType == this.NotificationFilter; - } - - 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"] = 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\""; - - // Search target - values["ST"] = "ssdp:all"; - - // Seconds to delay response - values["MX"] = "3"; - - var message = BuildMessage(header, values); - - return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken); - } - - private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IPAddress) - { - if (!message.IsSuccessStatusCode) - { - return; - } - - var location = GetFirstHeaderUriValue("Location", message); - if (location is not null) - { - var device = new DiscoveredSsdpDevice() - { - DescriptionLocation = location, - Usn = GetFirstHeaderStringValue("USN", message), - NotificationType = GetFirstHeaderStringValue("ST", message), - CacheLifetime = CacheAgeFromHeader(message.Headers.CacheControl), - AsAt = DateTimeOffset.Now, - ResponseHeaders = message.Headers - }; - - AddOrUpdateDiscoveredDevice(device, IPAddress); - } - } - - private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress) - { - 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) - { - ProcessAliveNotification(message, IPAddress); - } - else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) - { - ProcessByeByeNotification(message); - } - } - - private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IPAddress) - { - var location = GetFirstHeaderUriValue("Location", message); - if (location is not null) - { - var device = new DiscoveredSsdpDevice() - { - DescriptionLocation = location, - Usn = GetFirstHeaderStringValue("USN", message), - NotificationType = GetFirstHeaderStringValue("NT", message), - CacheLifetime = CacheAgeFromHeader(message.Headers.CacheControl), - AsAt = DateTimeOffset.Now, - ResponseHeaders = message.Headers - }; - - AddOrUpdateDiscoveredDevice(device, IPAddress); - } - } - - private void ProcessByeByeNotification(HttpRequestMessage message) - { - var notficationType = GetFirstHeaderStringValue("NT", message); - if (!string.IsNullOrEmpty(notficationType)) - { - var usn = GetFirstHeaderStringValue("USN", message); - - if (!DeviceDied(usn, false)) - { - var deadDevice = new DiscoveredSsdpDevice() - { - AsAt = DateTime.UtcNow, - CacheLifetime = TimeSpan.Zero, - DescriptionLocation = null, - NotificationType = GetFirstHeaderStringValue("NT", message), - Usn = usn, - ResponseHeaders = message.Headers - }; - - if (NotificationTypeMatchesFilter(deadDevice)) - { - OnDeviceUnavailable(deadDevice, false); - } - } - } - } - - private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) - { - string retVal = null; - if (message.Headers.Contains(headerName)) - { - message.Headers.TryGetValues(headerName, out var values); - if (values is not null) - { - retVal = values.FirstOrDefault(); - } - } - - return retVal; - } - - private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) - { - string retVal = null; - if (message.Headers.Contains(headerName)) - { - message.Headers.TryGetValues(headerName, out var values); - if (values is not null) - { - retVal = values.FirstOrDefault(); - } - } - - return retVal; - } - - private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) - { - string value = null; - if (request.Headers.Contains(headerName)) - { - request.Headers.TryGetValues(headerName, out var values); - if (values is not null) - { - value = values.FirstOrDefault(); - } - } - - Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal); - return retVal; - } - - private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) - { - string value = null; - if (response.Headers.Contains(headerName)) - { - response.Headers.TryGetValues(headerName, out var values); - if (values is not null) - { - value = values.FirstOrDefault(); - } - } - - Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal); - return retVal; - } - - private TimeSpan CacheAgeFromHeader(System.Net.Http.Headers.CacheControlHeaderValue headerValue) - { - if (headerValue is null) - { - return TimeSpan.Zero; - } - - return headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero; - } - - private void RemoveExpiredDevicesFromCache() - { - DiscoveredSsdpDevice[] expiredDevices = null; - lock (_Devices) - { - expiredDevices = (from device in _Devices where device.IsExpired() select device).ToArray(); - - foreach (var device in expiredDevices) - { - if (IsDisposed) - { - return; - } - - _Devices.Remove(device); - } - } - - // Don't do this inside lock because DeviceDied raises an event - // which means public code may execute during lock and cause - // problems. - foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct()) - { - if (IsDisposed) - { - return; - } - - DeviceDied(expiredUsn, true); - } - } - - private bool DeviceDied(string deviceUsn, bool expired) - { - List<DiscoveredSsdpDevice> existingDevices = null; - lock (_Devices) - { - existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); - foreach (var existingDevice in existingDevices) - { - if (IsDisposed) - { - return true; - } - - _Devices.Remove(existingDevice); - } - } - - if (existingDevices is not null && existingDevices.Count > 0) - { - foreach (var removedDevice in existingDevices) - { - if (NotificationTypeMatchesFilter(removedDevice)) - { - OnDeviceUnavailable(removedDevice, expired); - } - } - - return true; - } - - return false; - } - - private TimeSpan SearchTimeToMXValue(TimeSpan searchWaitTime) - { - if (searchWaitTime.TotalSeconds < 2 || searchWaitTime == TimeSpan.Zero) - { - return OneSecond; - } - - return searchWaitTime.Subtract(OneSecond); - } - - private DiscoveredSsdpDevice FindExistingDeviceNotification(IEnumerable<DiscoveredSsdpDevice> devices, string notificationType, string usn) - { - foreach (var d in devices) - { - if (d.NotificationType == notificationType && d.Usn == usn) - { - return d; - } - } - - return null; - } - - private List<DiscoveredSsdpDevice> FindExistingDeviceNotifications(IList<DiscoveredSsdpDevice> devices, string usn) - { - var list = new List<DiscoveredSsdpDevice>(); - - foreach (var d in devices) - { - if (d.Usn == usn) - { - list.Add(d); - } - } - - return list; - } - - private void CommsServer_ResponseReceived(object sender, ResponseReceivedEventArgs e) - { - ProcessSearchResponseMessage(e.Message, e.LocalIPAddress); - } - - private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e) - { - ProcessNotificationMessage(e.Message, e.ReceivedFrom.Address); - } - } -} diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs deleted file mode 100644 index 0ac9cc9a1..000000000 --- a/RSSDP/SsdpDevicePublisher.cs +++ /dev/null @@ -1,623 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Provides the platform independent logic for publishing SSDP devices (notifications and search responses). - /// </summary> - public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher - { - private ISsdpCommunicationsServer _CommsServer; - private string _OSName; - private string _OSVersion; - private bool _sendOnlyMatchedHost; - - private bool _SupportPnpRootDevice; - - private IList<SsdpRootDevice> _Devices; - private IReadOnlyList<SsdpRootDevice> _ReadOnlyDevices; - - private Timer _RebroadcastAliveNotificationsTimer; - - private IDictionary<string, SearchRequest> _RecentSearchRequests; - - private Random _Random; - - /// <summary> - /// Default constructor. - /// </summary> - public SsdpDevicePublisher( - ISsdpCommunicationsServer communicationsServer, - string osName, - string osVersion, - bool sendOnlyMatchedHost) - { - ArgumentNullException.ThrowIfNull(communicationsServer); - ArgumentNullException.ThrowIfNullOrEmpty(osName); - ArgumentNullException.ThrowIfNullOrEmpty(osVersion); - - _SupportPnpRootDevice = true; - _Devices = new List<SsdpRootDevice>(); - _ReadOnlyDevices = new ReadOnlyCollection<SsdpRootDevice>(_Devices); - _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase); - _Random = new Random(); - - _CommsServer = communicationsServer; - _CommsServer.RequestReceived += CommsServer_RequestReceived; - _OSName = osName; - _OSVersion = osVersion; - _sendOnlyMatchedHost = sendOnlyMatchedHost; - - _CommsServer.BeginListeningForMulticast(); - - // Send alive notification once on creation - SendAllAliveNotifications(null); - } - - public void StartSendingAliveNotifications(TimeSpan interval) - { - _RebroadcastAliveNotificationsTimer = new Timer(SendAllAliveNotifications, null, TimeSpan.FromSeconds(5), interval); - } - - /// <summary> - /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients. - /// </summary> - /// <remarks> - /// <para>Adding a device causes "alive" notification messages to be sent immediately, or very soon after. Ensure your device/description service is running before adding the device object here.</para> - /// <para>Devices added here with a non-zero cache life time will also have notifications broadcast periodically.</para> - /// <para>This method ignores duplicate device adds (if the same device instance is added multiple times, the second and subsequent add calls do nothing).</para> - /// </remarks> - /// <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> - public void AddDevice(SsdpRootDevice device) - { - if (device is null) - { - throw new ArgumentNullException(nameof(device)); - } - - ThrowIfDisposed(); - - bool wasAdded = false; - lock (_Devices) - { - if (!_Devices.Contains(device)) - { - _Devices.Add(device); - wasAdded = true; - } - } - - if (wasAdded) - { - WriteTrace("Device Added", device); - - SendAliveNotifications(device, true, CancellationToken.None); - } - } - - /// <summary> - /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable. - /// </summary> - /// <remarks> - /// <para>Removing a device causes "byebye" notification messages to be sent immediately, advising clients of the device/service becoming unavailable. We recommend removing the device from the published list before shutting down the actual device/service, if possible.</para> - /// <para>This method does nothing if the device was not found in the collection.</para> - /// </remarks> - /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - public async Task RemoveDevice(SsdpRootDevice device) - { - if (device is null) - { - throw new ArgumentNullException(nameof(device)); - } - - bool wasRemoved = false; - lock (_Devices) - { - if (_Devices.Contains(device)) - { - _Devices.Remove(device); - wasRemoved = true; - } - } - - if (wasRemoved) - { - WriteTrace("Device Removed", device); - - await SendByeByeNotifications(device, true, CancellationToken.None).ConfigureAwait(false); - } - } - - /// <summary> - /// Returns a read only list of devices being published by this instance. - /// </summary> - public IEnumerable<SsdpRootDevice> Devices - { - get - { - return _ReadOnlyDevices; - } - } - - /// <summary> - /// If true (default) treats root devices as both upnp:rootdevice and pnp:rootdevice types. - /// </summary> - /// <remarks> - /// <para>Enabling this option will cause devices to show up in Microsoft Windows Explorer's network screens (if discovery is enabled etc.). Windows Explorer appears to search only for pnp:rootdeivce and not upnp:rootdevice.</para> - /// <para>If false, the system will only use upnp:rootdevice for notification broadcasts and and search responses, which is correct according to the UPnP/SSDP spec.</para> - /// </remarks> - public bool SupportPnpRootDevice - { - get { return _SupportPnpRootDevice; } - - set - { - _SupportPnpRootDevice = value; - } - } - - /// <summary> - /// Stops listening for requests, stops sending periodic broadcasts, disposes all internal resources. - /// </summary> - /// <param name="disposing"></param> - protected override void Dispose(bool disposing) - { - if (disposing) - { - DisposeRebroadcastTimer(); - - var commsServer = _CommsServer; - if (commsServer is not null) - { - commsServer.RequestReceived -= this.CommsServer_RequestReceived; - } - - var tasks = Devices.ToList().Select(RemoveDevice).ToArray(); - Task.WaitAll(tasks); - - _CommsServer = null; - if (commsServer is not null) - { - if (!commsServer.IsShared) - { - commsServer.Dispose(); - } - } - - _RecentSearchRequests = null; - } - } - - private void ProcessSearchRequest( - string mx, - string searchTarget, - IPEndPoint remoteEndPoint, - IPAddress receivedOnlocalIPAddress, - CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(searchTarget)) - { - WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); - return; - } - - // WriteTrace(String.Format("Search Request Received From {0}, Target = {1}", remoteEndPoint.ToString(), searchTarget)); - - if (IsDuplicateSearchRequest(searchTarget, remoteEndPoint)) - { - // WriteTrace("Search Request is Duplicate, ignoring."); - return; - } - - // 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. - if (String.IsNullOrEmpty(mx)) - { - // Windows Explorer is poorly behaved and doesn't supply an MX header value. - // if (this.SupportPnpRootDevice) - mx = "1"; - // else - // return; - } - - if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) - { - return; - } - - if (maxWaitInterval > 120) - { - maxWaitInterval = _Random.Next(0, 120); - } - - // Do not block synchronously as that may tie up a threadpool thread for several seconds. - 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; - lock (_Devices) - { - if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) - { - devices = GetAllDevicesAsFlatEnumerable().ToArray(); - } - 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(); - } - else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) - { - devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); - } - } - - if (devices is not null) - { - // WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); - - foreach (var device in devices) - { - var root = device.ToRootDevice(); - - if (!_sendOnlyMatchedHost || root.Address.Equals(receivedOnlocalIPAddress)) - { - SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIPAddress, cancellationToken); - } - } - } - }, cancellationToken); - } - - private IEnumerable<SsdpDevice> GetAllDevicesAsFlatEnumerable() - { - return _Devices.Union(_Devices.SelectManyRecursive<SsdpDevice>((d) => d.Devices)); - } - - private void SendDeviceSearchResponses( - SsdpDevice device, - IPEndPoint endPoint, - 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); - if (SupportPnpRootDevice) - { - SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); - } - } - - SendSearchResponse(device.Udn, device, device.Udn, endPoint, receivedOnlocalIPAddress, cancellationToken); - - SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint, receivedOnlocalIPAddress, cancellationToken); - } - - private string GetUsn(string udn, string fullDeviceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); - } - - private async void SendSearchResponse( - string searchTarget, - SsdpDevice device, - string uniqueServiceName, - IPEndPoint endPoint, - IPAddress receivedOnlocalIPAddress, - CancellationToken cancellationToken) - { - const string header = "HTTP/1.1 200 OK"; - - var rootDevice = device.ToRootDevice(); - 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); - - try - { - await _CommsServer.SendMessage( - Encoding.UTF8.GetBytes(message), - endPoint, - receivedOnlocalIPAddress, - cancellationToken) - .ConfigureAwait(false); - } - catch (Exception) - { - } - - // WriteTrace(String.Format("Sent search response to " + endPoint.ToString()), device); - } - - private bool IsDuplicateSearchRequest(string searchTarget, IPEndPoint endPoint) - { - var isDuplicateRequest = false; - - var newRequest = new SearchRequest() { EndPoint = endPoint, SearchTarget = searchTarget, Received = DateTime.UtcNow }; - lock (_RecentSearchRequests) - { - if (_RecentSearchRequests.ContainsKey(newRequest.Key)) - { - var lastRequest = _RecentSearchRequests[newRequest.Key]; - if (lastRequest.IsOld()) - { - _RecentSearchRequests[newRequest.Key] = newRequest; - } - else - { - isDuplicateRequest = true; - } - } - else - { - _RecentSearchRequests.Add(newRequest.Key, newRequest); - if (_RecentSearchRequests.Count > 10) - { - CleanUpRecentSearchRequestsAsync(); - } - } - } - - return isDuplicateRequest; - } - - private void CleanUpRecentSearchRequestsAsync() - { - lock (_RecentSearchRequests) - { - foreach (var requestKey in (from r in _RecentSearchRequests where r.Value.IsOld() select r.Key).ToArray()) - { - _RecentSearchRequests.Remove(requestKey); - } - } - } - - private void SendAllAliveNotifications(object state) - { - try - { - if (IsDisposed) - { - return; - } - - // WriteTrace("Begin Sending Alive Notifications For All Devices"); - - SsdpRootDevice[] devices; - lock (_Devices) - { - devices = _Devices.ToArray(); - } - - foreach (var device in devices) - { - if (IsDisposed) - { - return; - } - - SendAliveNotifications(device, true, CancellationToken.None); - } - - // WriteTrace("Completed Sending Alive Notifications For All Devices"); - } - catch (ObjectDisposedException ex) - { - WriteTrace("Publisher stopped, exception " + ex.Message); - Dispose(); - } - } - - private void SendAliveNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken) - { - if (isRoot) - { - SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); - if (SupportPnpRootDevice) - { - SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); - } - } - - SendAliveNotification(device, device.Udn, device.Udn, cancellationToken); - SendAliveNotification(device, device.FullDeviceType, GetUsn(device.Udn, device.FullDeviceType), cancellationToken); - - foreach (var childDevice in device.Devices) - { - SendAliveNotifications(childDevice, false, cancellationToken); - } - } - - private void SendAliveNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) - { - var rootDevice = device.ToRootDevice(); - - 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 - ["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); - - _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken); - - // WriteTrace(String.Format("Sent alive notification"), device); - } - - private Task SendByeByeNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken) - { - var tasks = new List<Task>(); - if (isRoot) - { - tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); - if (SupportPnpRootDevice) - { - tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken)); - } - } - - tasks.Add(SendByeByeNotification(device, device.Udn, device.Udn, cancellationToken)); - tasks.Add(SendByeByeNotification(device, String.Format(CultureInfo.InvariantCulture, "urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken)); - - foreach (var childDevice in device.Devices) - { - tasks.Add(SendByeByeNotifications(childDevice, false, cancellationToken)); - } - - return Task.WhenAll(tasks); - } - - private Task SendByeByeNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) - { - 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 - ["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); - return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); - } - - private void DisposeRebroadcastTimer() - { - var timer = _RebroadcastAliveNotificationsTimer; - _RebroadcastAliveNotificationsTimer = null; - timer?.Dispose(); - } - - private TimeSpan GetMinimumNonZeroCacheLifetime() - { - var nonzeroCacheLifetimesQuery = ( - from device - in _Devices - where device.CacheLifetime != TimeSpan.Zero - select device.CacheLifetime).ToList(); - - if (nonzeroCacheLifetimesQuery.Any()) - { - return nonzeroCacheLifetimesQuery.Min(); - } - - return TimeSpan.Zero; - } - - private string GetFirstHeaderValue(System.Net.Http.Headers.HttpRequestHeaders httpRequestHeaders, string headerName) - { - string retVal = null; - if (httpRequestHeaders.TryGetValues(headerName, out var values) && values is not null) - { - retVal = values.FirstOrDefault(); - } - - return retVal; - } - - public Action<string> LogFunction { get; set; } - - private void WriteTrace(string text) - { - LogFunction?.Invoke(text); - // System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher"); - } - - private void WriteTrace(string text, SsdpDevice device) - { - var rootDevice = device as SsdpRootDevice; - if (rootDevice is not null) - { - WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid + " - " + rootDevice.Location); - } - else - { - WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid); - } - } - - private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e) - { - if (this.IsDisposed) - { - return; - } - - if (string.Equals(e.Message.Method.Method, SsdpConstants.MSearchMethod, StringComparison.OrdinalIgnoreCase)) - { - // According to SSDP/UPnP spec, ignore message if missing these headers. - // Edit: But some devices do it anyway - // if (!e.Message.Headers.Contains("MX")) - // WriteTrace("Ignoring search request - missing MX header."); - // 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); - } - } - - private class SearchRequest - { - public IPEndPoint EndPoint { get; set; } - - public DateTime Received { get; set; } - - public string SearchTarget { get; set; } - - public string Key - { - get { return this.SearchTarget + ":" + this.EndPoint; } - } - - public bool IsOld() - { - return DateTime.UtcNow.Subtract(this.Received).TotalMilliseconds > 500; - } - } - } -} diff --git a/RSSDP/SsdpEmbeddedDevice.cs b/RSSDP/SsdpEmbeddedDevice.cs deleted file mode 100644 index f1a598111..000000000 --- a/RSSDP/SsdpEmbeddedDevice.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Rssdp -{ - /// <summary> - /// Represents a device that is a descendant of a <see cref="SsdpRootDevice"/> instance. - /// </summary> - public class SsdpEmbeddedDevice : SsdpDevice - { - private SsdpRootDevice _RootDevice; - - /// <summary> - /// Default constructor. - /// </summary> - public SsdpEmbeddedDevice() - { - } - - /// <summary> - /// Returns the <see cref="SsdpRootDevice"/> that is this device's first ancestor. If this device is itself an <see cref="SsdpRootDevice"/>, then returns a reference to itself. - /// </summary> - public SsdpRootDevice RootDevice - { - get - { - return _RootDevice; - } - - internal set - { - _RootDevice = value; - lock (this.Devices) - { - foreach (var embeddedDevice in this.Devices) - { - ((SsdpEmbeddedDevice)embeddedDevice).RootDevice = _RootDevice; - } - } - } - } - } -} diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs deleted file mode 100644 index 5ecb1f86f..000000000 --- a/RSSDP/SsdpRootDevice.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Net; - -namespace Rssdp -{ - /// <summary> - /// Represents a 'root' device, a device that has no parent. Used for publishing devices and for the root device in a tree of discovered devices. - /// </summary> - /// <remarks> - /// <para>Child (embedded) devices are represented by the <see cref="SsdpDevice"/> in the <see cref="SsdpDevice.Devices"/> property.</para> - /// <para>Root devices contain some information that applies to the whole device tree and is therefore not present on child devices, such as <see cref="CacheLifetime"/> and <see cref="Location"/>.</para> - /// </remarks> - public class SsdpRootDevice : SsdpDevice - { - private Uri _UrlBase; - - /// <summary> - /// Default constructor. - /// </summary> - public SsdpRootDevice() : base() - { - } - - /// <summary> - /// Specifies how long clients can cache this device's details for. Optional but defaults to <see cref="TimeSpan.Zero"/> which means no-caching. Recommended value is half an hour. - /// </summary> - /// <remarks> - /// <para>Specify <see cref="TimeSpan.Zero"/> to indicate no caching allowed.</para> - /// <para>Also used to specify how often to rebroadcast alive notifications.</para> - /// <para>The UPnP/SSDP specifications indicate this should not be less than 1800 seconds (half an hour), but this is not enforced by this library.</para> - /// </remarks> - public TimeSpan CacheLifetime - { - get; set; - } - - /// <summary> - /// Gets or sets the URL used to retrieve the description document for this device/tree. Required. - /// </summary> - public Uri Location { get; set; } - - /// <summary> - /// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required. - /// </summary> - public IPAddress Address { get; set; } - - /// <summary> - /// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required. - /// </summary> - public byte PrefixLength { get; set; } - - /// <summary> - /// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional. - /// </summary> - /// <remarks> - /// <para>Defines the base URL. Used to construct fully-qualified URLs. All relative URLs that appear elsewhere in the description are combined with this base URL. If URLBase is empty or not given, the base URL is the URL from which the device description was retrieved (which is the preferred implementation; use of URLBase is no longer recommended). Specified by UPnP vendor. Single URL.</para> - /// </remarks> - public Uri UrlBase - { - get - { - return _UrlBase ?? this.Location; - } - - set - { - _UrlBase = value; - } - } - } -} diff --git a/src/Jellyfin.Networking/ExternalPortForwarding.cs b/src/Jellyfin.Networking/ExternalPortForwarding.cs new file mode 100644 index 000000000..df9e43ca9 --- /dev/null +++ b/src/Jellyfin.Networking/ExternalPortForwarding.cs @@ -0,0 +1,195 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Logging; +using Mono.Nat; + +namespace Jellyfin.Networking; + +/// <summary> +/// Server entrypoint handling external port forwarding. +/// </summary> +public sealed class ExternalPortForwarding : IServerEntryPoint +{ + private readonly IServerApplicationHost _appHost; + private readonly ILogger<ExternalPortForwarding> _logger; + private readonly IServerConfigurationManager _config; + + private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>(); + + private Timer _timer; + private string _configIdentifier; + + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="appHost">The application host.</param> + /// <param name="config">The configuration manager.</param> + public ExternalPortForwarding( + ILogger<ExternalPortForwarding> logger, + IServerApplicationHost appHost, + IServerConfigurationManager config) + { + _logger = logger; + _appHost = appHost; + _config = config; + } + + private string GetConfigIdentifier() + { + const char Separator = '|'; + var config = _config.GetNetworkConfiguration(); + + return new StringBuilder(32) + .Append(config.EnableUPnP).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) + .Append(config.EnableRemoteAccess).Append(Separator) + .ToString(); + } + + private void OnConfigurationUpdated(object sender, EventArgs e) + { + var oldConfigIdentifier = _configIdentifier; + _configIdentifier = GetConfigIdentifier(); + + if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase)) + { + Stop(); + Start(); + } + } + + /// <inheritdoc /> + public Task RunAsync() + { + Start(); + + _config.ConfigurationUpdated += OnConfigurationUpdated; + + return Task.CompletedTask; + } + + private void Start() + { + var config = _config.GetNetworkConfiguration(); + if (!config.EnableUPnP || !config.EnableRemoteAccess) + { + return; + } + + _logger.LogInformation("Starting NAT discovery"); + + NatUtility.DeviceFound += OnNatUtilityDeviceFound; + NatUtility.StartDiscovery(); + + _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); + } + + private void Stop() + { + _logger.LogInformation("Stopping NAT discovery"); + + NatUtility.StopDiscovery(); + NatUtility.DeviceFound -= OnNatUtilityDeviceFound; + + _timer?.Dispose(); + } + + private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) + { + try + { + await CreateRules(e.Device).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating port forwarding rules"); + } + } + + private Task CreateRules(INatDevice device) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + // On some systems the device discovered event seems to fire repeatedly + // This check will help ensure we're not trying to port map the same device over and over + if (!_createdRules.TryAdd(device.DeviceEndpoint, 0)) + { + return Task.CompletedTask; + } + + return Task.WhenAll(CreatePortMaps(device)); + } + + private IEnumerable<Task> CreatePortMaps(INatDevice device) + { + var config = _config.GetNetworkConfiguration(); + yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort); + + if (_appHost.ListenWithHttps) + { + yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); + } + } + + private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort) + { + _logger.LogDebug( + "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}", + privatePort, + publicPort, + device.DeviceEndpoint); + + try + { + var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name); + await device.CreatePortMapAsync(mapping).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.", + privatePort, + publicPort, + device.DeviceEndpoint); + } + } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + _config.ConfigurationUpdated -= OnConfigurationUpdated; + + Stop(); + + _timer?.Dispose(); + _timer = null; + + _disposed = true; + } +} diff --git a/src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs new file mode 100644 index 000000000..7d86434b8 --- /dev/null +++ b/src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -0,0 +1,119 @@ +/* +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; +using System.Threading; +using System.Threading.Tasks; + +namespace Jellyfin.Networking.HappyEyeballs; + +/// <summary> +/// Defines the <see cref="HttpClientExtension"/> class. +/// +/// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 . +/// </summary> +public static class HttpClientExtension +{ + /// <summary> + /// Gets or sets a value indicating whether the client should use IPv6. + /// </summary> + public static bool UseIPv6 { get; set; } = true; + + /// <summary> + /// Implements the httpclient callback method. + /// </summary> + /// <param name="context">The <see cref="SocketsHttpConnectionContext"/> instance.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/> instance.</param> + /// <returns>The http steam.</returns> + public static async ValueTask<Stream> OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + if (!UseIPv6) + { + return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); + } + + using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); + + // 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) + { + await cancelIPv6.CancelAsync().ConfigureAwait(false); + 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) + { + await cancelIPv4.CancelAsync().ConfigureAwait(false); + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } + + return tryConnectAsyncIPv4.GetAwaiter().GetResult(); + } + else + { + if (tryConnectAsyncIPv4.IsCompletedSuccessfully) + { + await cancelIPv6.CancelAsync().ConfigureAwait(false); + return tryConnectAsyncIPv4.GetAwaiter().GetResult(); + } + + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } + } + + private static async Task<Stream> 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/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj new file mode 100644 index 000000000..24b3ecaab --- /dev/null +++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <ItemGroup> + <Compile Include="..\..\SharedVersion.cs" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Mono.Nat" /> + </ItemGroup> +</Project> diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs new file mode 100644 index 000000000..1da44b048 --- /dev/null +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -0,0 +1,1125 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading; +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; + +/// <summary> +/// Class to take care of network interface management. +/// </summary> +public class NetworkManager : INetworkManager, IDisposable +{ + /// <summary> + /// Threading lock for network properties. + /// </summary> + private readonly object _initLock; + + private readonly ILogger<NetworkManager> _logger; + + 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 IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls; + + 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 IReadOnlyList<IPData> _interfaces; + + /// <summary> + /// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>) + /// or internal interface network subnets if undefined by user. + /// </summary> + private IReadOnlyList<IPNetwork> _lanSubnets; + + /// <summary> + /// User defined list of subnets to excluded from the LAN. + /// </summary> + private IReadOnlyList<IPNetwork> _excludedSubnets; + + /// <summary> + /// True if this object is disposed. + /// </summary> + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="NetworkManager"/> class. + /// </summary> + /// <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, 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 List<PublishedServerUriOverride>(); + _networkEventLock = new object(); + _remoteAddressFilter = new List<IPNetwork>(); + + UpdateSettings(_configurationManager.GetNetworkConfiguration()); + + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated; + } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. + + /// <summary> + /// Event triggered on network changes. + /// </summary> + public event EventHandler? NetworkChanged; + + /// <summary> + /// Gets or sets a value indicating whether testing is taking place. + /// </summary> + public static string MockNetworkSettings { get; set; } = string.Empty; + + /// <summary> + /// Gets a value indicating whether IP4 is enabled. + /// </summary> + public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4; + + /// <summary> + /// Gets a value indicating whether IP6 is enabled. + /// </summary> + public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6; + + /// <summary> + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// </summary> + public bool TrustAllIPv6Interfaces { get; private set; } + + /// <summary> + /// Gets the Published server override list. + /// </summary> + public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls; + + /// <inheritdoc/> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Handler for network change events. + /// </summary> + /// <param name="sender">Sender.</param> + /// <param name="e">A <see cref="NetworkAvailabilityEventArgs"/> containing network availability information.</param> + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + _logger.LogDebug("Network availability changed."); + HandleNetworkChange(); + } + + /// <summary> + /// Handler for network change events. + /// </summary> + /// <param name="sender">Sender.</param> + /// <param name="e">An <see cref="EventArgs"/>.</param> + private void OnNetworkAddressChanged(object? sender, EventArgs e) + { + _logger.LogDebug("Network address change detected."); + HandleNetworkChange(); + } + + /// <summary> + /// Triggers our event, and re-loads interface information. + /// </summary> + private void HandleNetworkChange() + { + lock (_networkEventLock) + { + if (!_eventfire) + { + // As network events tend to fire one after the other only fire once every second. + _eventfire = true; + OnNetworkChange(); + } + } + } + + /// <summary> + /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. + /// </summary> + private void OnNetworkChange() + { + try + { + Thread.Sleep(2000); + var networkConfig = _configurationManager.GetNetworkConfiguration(); + if (IsIPv6Enabled && !Socket.OSSupportsIPv6) + { + UpdateSettings(networkConfig); + } + else + { + InitializeInterfaces(); + InitializeLan(networkConfig); + EnforceBindSettings(networkConfig); + } + + PrintNetworkInformation(networkConfig); + NetworkChanged?.Invoke(this, EventArgs.Empty); + } + finally + { + _eventfire = false; + } + } + + /// <summary> + /// 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 InitializeInterfaces() + { + lock (_initLock) + { + _logger.LogDebug("Refreshing interfaces."); + + var interfaces = new List<IPData>(); + var macAddresses = new List<PhysicalAddress>(); + + try + { + var nics = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => i.OperationalStatus == OperationalStatus.Up); + + foreach (NetworkInterface adapter in nics) + { + try + { + var ipProperties = adapter.GetIPProperties(); + var mac = adapter.GetPhysicalAddress(); + + // Populate MAC list + if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac)) + { + macAddresses.Add(mac); + } + + // Populate interface list + foreach (var info in ipProperties.UnicastAddresses) + { + if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + { + 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) + { + Index = ipProperties.GetIPv6Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; + + interfaces.Add(interfaceObject); + } + } + } + catch (Exception ex) + { + // Ignore error, and attempt to continue. + _logger.LogError(ex, "Error encountered parsing interfaces."); + } + } + } + catch (Exception ex) + { + _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)."); + + if (IsIPv4Enabled) + { + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); + } + + if (IsIPv6Enabled) + { + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "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())); + + _macAddresses = macAddresses; + _interfaces = interfaces; + } + } + + /// <summary> + /// Initializes internal LAN cache. + /// </summary> + private void InitializeLan(NetworkConfiguration config) + { + lock (_initLock) + { + _logger.LogDebug("Refreshing LAN information."); + + // Get configuration options + var subnets = config.LocalNetworkSubnets; + + // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN + if (!NetworkUtils.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) + { + 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(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; + } + else + { + _lanSubnets = lanSubnets; + } + + _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true) + ? excludedSubnets + : new List<IPNetwork>(); + } + } + + /// <summary> + /// Enforce bind addresses and exclusions on available interfaces. + /// </summary> + private void EnforceBindSettings(NetworkConfiguration config) + { + 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 => NetworkUtils.TryParseToSubnet(p, out var network) + ? network.Prefix + : (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(); + + if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) + { + 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, NetworkConstants.IPv6RFC4291Loopback, "lo")); + } + } + + // Remove all interfaces matching any virtual machine interface prefix + if (config.IgnoreVirtualInterfaces) + { + // Remove potentially existing * and split config string into prefixes + var virtualInterfacePrefixes = config.VirtualInterfaceNames + .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); + + // Check all interfaces for matches against the prefixes and remove them + if (_interfaces.Count > 0) + { + foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) + { + interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); + } + } + } + + // 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); + } + + _interfaces = interfaces; + } + } + + /// <summary> + /// Initializes the remote address values. + /// </summary> + private void InitializeRemote(NetworkConfiguration config) + { + lock (_initLock) + { + // Parse config values into filter collection + var remoteIPFilter = config.RemoteIPFilter; + if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0])) + { + // Parse all IPs with netmask to a subnet + var remoteAddressFilter = new List<IPNetwork>(); + var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(); + if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) + { + remoteAddressFilter = remoteAddressFilterResult.ToList(); + } + + // Parse everything else as an IP and construct subnet with a single IP + var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase)); + foreach (var ip in remoteFilteredIPs) + { + if (IPAddress.TryParse(ip, out var ipp)) + { + remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize)); + } + } + + _remoteAddressFilter = remoteAddressFilter; + } + } + } + + /// <summary> + /// 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. + /// </summary> + private void InitializeOverrides(NetworkConfiguration config) + { + lock (_initLock) + { + var publishedServerUrls = new List<PublishedServerUriOverride>(); + + // Prefer startup configuration. + var startupOverrideKey = _startupConfig[AddressOverrideKey]; + if (!string.IsNullOrEmpty(startupOverrideKey)) + { + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), + startupOverrideKey, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), + startupOverrideKey, + true, + true)); + _publishedServerUrls = publishedServerUrls; + return; + } + + var 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); + return; + } + + var replacement = parts[1].Trim(); + var identifier = parts[0]; + if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) + { + // Drop any other overrides in case an "all" override exists + publishedServerUrls.Clear(); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), + replacement, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), + replacement, + true, + true)); + break; + } + else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) + { + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), + replacement, + false, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), + replacement, + false, + true)); + } + else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) + { + foreach (var lan in _lanSubnets) + { + var lanPrefix = lan.Prefix; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), + replacement, + true, + false)); + } + } + else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) + { + var data = new IPData(result.Prefix, result); + publishedServerUrls.Add( + new PublishedServerUriOverride( + data, + replacement, + true, + true)); + } + else if (TryParseInterface(identifier, out var ifaces)) + { + foreach (var iface in ifaces) + { + publishedServerUrls.Add( + new PublishedServerUriOverride( + iface, + replacement, + true, + true)); + } + } + else + { + _logger.LogError("Unable to parse bind override: {Entry}", entry); + } + } + + _publishedServerUrls = publishedServerUrls; + } + } + + private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) + { + if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal)) + { + UpdateSettings((NetworkConfiguration)evt.NewConfiguration); + } + } + + /// <summary> + /// Reloads all settings and re-Initializes the instance. + /// </summary> + /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param> + public void UpdateSettings(object configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + + var config = (NetworkConfiguration)configuration; + HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; + + InitializeLan(config); + InitializeRemote(config); + + if (string.IsNullOrEmpty(MockNetworkSettings)) + { + InitializeInterfaces(); + } + else // Used in testing only. + { + // 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(','); + if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) + { + 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]) + { + Index = index + }; + interfaces.Add(data); + } + } + else + { + _logger.LogWarning("Could not parse mock interface settings: {Part}", details); + } + } + + _interfaces = interfaces; + } + + EnforceBindSettings(config); + InitializeOverrides(config); + + PrintNetworkInformation(config, false); + } + + /// <summary> + /// Protected implementation of Dispose pattern. + /// </summary> + /// <param name="disposing"><c>True</c> to dispose the managed state.</param> + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; + NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + } + + _disposed = true; + } + } + + /// <inheritdoc/> + public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result) + { + if (string.IsNullOrEmpty(intf) + || _interfaces is null + || _interfaces.Count == 0) + { + result = null; + return false; + } + + // Match all interfaces starting with names starting with token + 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/> + 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.Any(x => x.Contains(remoteIP))) + { + // Remote not enabled. So everyone should be LAN. + return false; + } + + return true; + } + + /// <inheritdoc/> + public IReadOnlyList<PhysicalAddress> GetMacAddresses() + { + // Populated in construction - so always has values. + return _macAddresses; + } + + /// <inheritdoc/> + public IReadOnlyList<IPData> GetLoopbacks() + { + if (!IsIPv4Enabled && !IsIPv6Enabled) + { + return Array.Empty<IPData>(); + } + + var loopbackNetworks = new List<IPData>(); + if (IsIPv4Enabled) + { + loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); + } + + if (IsIPv6Enabled) + { + loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); + } + + return loopbackNetworks; + } + + /// <inheritdoc/> + public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) + { + if (_interfaces.Count > 0 || individualInterfaces) + { + return _interfaces; + } + + // No bind address and no exclusions, so listen on all interfaces. + var result = new List<IPData>(); + 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, NetworkConstants.IPv6Any)); + } + else if (IsIPv4Enabled) + { + result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any)); + } + 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 GetBindAddress(string source, out int? port) + { + if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) + { + addresses = Array.Empty<IPAddress>(); + } + + var result = GetBindAddress(addresses.FirstOrDefault(), out port); + return result; + } + + /// <inheritdoc/> + public string GetBindAddress(HttpRequest source, out int? port) + { + var result = GetBindAddress(source.Host.Host, out port); + port ??= source.Host.Port; + + return result; + } + + /// <inheritdoc/> + public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false) + { + port = null; + + string result; + + if (source is not null) + { + 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) + { + _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("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal); + + if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result)) + { + return result; + } + + // 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. + // 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) + .ToList(); + + if (availableInterfaces.Count == 0) + { + // 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; + } + + // If no source address is given, use the preferred (first) interface + if (source is null) + { + result = NetworkUtils.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 = 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 = NetworkUtils.FormatIPString(availableInterfaces[0].Address); + _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); + return result; + } + + /// <inheritdoc/> + public IReadOnlyList<IPData> GetInternalBindAddresses() + { + // Select all local bind addresses + return _interfaces.Where(x => IsInLocalNetwork(x.Address)) + .OrderBy(x => x.Index) + .ToList(); + } + + /// <inheritdoc/> + public bool IsInLocalNetwork(string address) + { + 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 (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) + { + foreach (var ept in addresses) + { + if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept)))) + { + return true; + } + } + } + + return false; + } + + /// <inheritdoc/> + public bool IsInLocalNetwork(IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + 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 + return CheckIfLanAndNotExcluded(address); + } + + private bool CheckIfLanAndNotExcluded(IPAddress address) + { + foreach (var lanSubnet in _lanSubnets) + { + if (lanSubnet.Contains(address)) + { + foreach (var excludedSubnet in _excludedSubnets) + { + if (excludedSubnet.Contains(address)) + { + return false; + } + } + + return true; + } + } + + return false; + } + + /// <summary> + /// Attempts to match the source against the published server URL overrides. + /// </summary> + /// <param name="source">IP source address to use.</param> + /// <param name="isInExternalSubnet">True if the source is in an external subnet.</param> + /// <param name="bindPreference">The published server URL that matches the source address.</param> + /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns> + private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference) + { + bindPreference = string.Empty; + int? port = null; + + // 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(); + } + + foreach (var data in validPublishedServerUrls) + { + // 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) + { + // If matching interface is found, use override + bindPreference = data.OverrideUri; + break; + } + } + + if (string.IsNullOrEmpty(bindPreference)) + { + _logger.LogDebug("{Source}: No matching bind address override found", source); + return false; + } + + // Handle override specifying port + var parts = bindPreference.Split(':'); + if (parts.Length > 1) + { + if (int.TryParse(parts[1], out int p)) + { + bindPreference = parts[0]; + port = p; + _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); + return true; + } + } + + _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); + return true; + } + + /// <summary> + /// Attempts to match the source against the user defined bind interfaces. + /// </summary> + /// <param name="source">IP source address to use.</param> + /// <param name="isInExternalSubnet">True if the source is in the external subnet.</param> + /// <param name="result">The result, if a match is found.</param> + /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns> + private bool MatchesBindInterface(IPAddress source, bool isInExternalSubnet, out string result) + { + result = string.Empty; + + 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) + { + return false; + } + + IPAddress? bindAddress = null; + if (isInExternalSubnet) + { + var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address)) + .OrderBy(x => x.Index) + .ToList(); + if (externalInterfaces.Count > 0) + { + // 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) + .First(); + + result = NetworkUtils.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)) + .OrderByDescending(x => x.Subnet.Contains(source)) + .ThenBy(x => x.Index) + .Select(x => x.Address) + .FirstOrDefault(); + + if (bindAddress is not null) + { + result = NetworkUtils.FormatIPString(bindAddress); + _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); + return true; + } + } + + return false; + } + + /// <summary> + /// Attempts to match the source against external interfaces. + /// </summary> + /// <param name="source">IP source address to use.</param> + /// <param name="result">The result, if a match is found.</param> + /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns> + private bool MatchesExternalInterface(IPAddress source, out string result) + { + // 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; + } + + // Does the request originate in one of the interface subnets? + // (For systems with multiple network cards and/or multiple subnets) + foreach (var intf in extResult) + { + if (intf.Subnet.Contains(source)) + { + 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 = NetworkUtils.FormatIPString(extResult[0].Address); + _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/src/Jellyfin.Networking/Udp/SocketFactory.cs b/src/Jellyfin.Networking/Udp/SocketFactory.cs new file mode 100644 index 000000000..f0267debc --- /dev/null +++ b/src/Jellyfin.Networking/Udp/SocketFactory.cs @@ -0,0 +1,38 @@ +using System; +using System.Net; +using System.Net.Sockets; +using MediaBrowser.Model.Net; + +namespace Jellyfin.Networking.Udp; + +/// <summary> +/// Factory class to create different kinds of sockets. +/// </summary> +public class SocketFactory : ISocketFactory +{ + /// <inheritdoc /> + public Socket CreateUdpBroadcastSocket(int localPort) + { + if (localPort < 0) + { + throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + } + + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + try + { + 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 socket; + } + catch + { + socket.Dispose(); + + throw; + } + } +} diff --git a/src/Jellyfin.Networking/Udp/UdpServer.cs b/src/Jellyfin.Networking/Udp/UdpServer.cs new file mode 100644 index 000000000..b130a5a5f --- /dev/null +++ b/src/Jellyfin.Networking/Udp/UdpServer.cs @@ -0,0 +1,136 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Model.ApiClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; + +namespace Jellyfin.Networking.Udp; + +/// <summary> +/// Provides a Udp Server. +/// </summary> +public sealed class UdpServer : IDisposable +{ + /// <summary> + /// The _logger. + /// </summary> + private readonly ILogger _logger; + private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _config; + + private readonly byte[] _receiveBuffer = new byte[8192]; + + private readonly Socket _udpSocket; + private readonly IPEndPoint _endpoint; + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="UdpServer" /> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="appHost">The application host.</param> + /// <param name="configuration">The configuration manager.</param> + /// <param name="bindAddress"> The bind address.</param> + /// <param name="port">The port.</param> + public UdpServer( + ILogger logger, + IServerApplicationHost appHost, + IConfiguration configuration, + IPAddress bindAddress, + int port) + { + _logger = logger; + _appHost = appHost; + _config = configuration; + + _endpoint = new IPEndPoint(bindAddress, port); + + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) + { + MulticastLoopback = false, + }; + _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + } + + private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken) + { + string? localUrl = _config[AddressOverrideKey]; + if (string.IsNullOrEmpty(localUrl)) + { + localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); + } + + if (string.IsNullOrEmpty(localUrl)) + { + _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined."); + return; + } + + var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); + + try + { + _logger.LogDebug("Sending AutoDiscovery response"); + await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); + } + catch (SocketException ex) + { + _logger.LogError(ex, "Error sending response message"); + } + } + + /// <summary> + /// Starts the specified port. + /// </summary> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + public void Start(CancellationToken cancellationToken) + { + _udpSocket.Bind(_endpoint); + + _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); + } + + private async Task BeginReceiveAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + 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)) + { + await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException ex) + { + _logger.LogError(ex, "Failed to receive data from socket"); + } + catch (OperationCanceledException) + { + _logger.LogDebug("Broadcast socket operation cancelled"); + } + } + } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + _udpSocket.Dispose(); + _disposed = true; + } +} diff --git a/src/Jellyfin.Networking/UdpServerEntryPoint.cs b/src/Jellyfin.Networking/UdpServerEntryPoint.cs new file mode 100644 index 000000000..61180c3c0 --- /dev/null +++ b/src/Jellyfin.Networking/UdpServerEntryPoint.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Networking.Udp; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; + +namespace Jellyfin.Networking; + +/// <summary> +/// Class responsible for registering all UDP broadcast endpoints and their handlers. +/// </summary> +public sealed class UdpServerEntryPoint : IServerEntryPoint +{ + /// <summary> + /// The port of the UDP server. + /// </summary> + public const int PortNumber = 7359; + + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<UdpServerEntryPoint> _logger; + private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _config; + private readonly IConfigurationManager _configurationManager; + private readonly INetworkManager _networkManager; + + /// <summary> + /// The UDP server. + /// </summary> + 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. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param> + /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param> + /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> + /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + public UdpServerEntryPoint( + ILogger<UdpServerEntryPoint> logger, + IServerApplicationHost appHost, + IConfiguration configuration, + IConfigurationManager configurationManager, + INetworkManager networkManager) + { + _logger = logger; + _appHost = appHost; + _config = configuration; + _configurationManager = configurationManager; + _networkManager = networkManager; + _udpServers = new List<UdpServer>(); + } + + /// <inheritdoc /> + public Task RunAsync() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery) + { + return Task.CompletedTask; + } + + try + { + // 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 listener + var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, 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 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); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); + } + } + else + { + // 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) + { + _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber); + } + + return Task.CompletedTask; + } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + foreach (var server in _udpServers) + { + server.Dispose(); + } + + _udpServers.Clear(); + _disposed = true; + } +} diff --git a/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs b/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs deleted file mode 100644 index 78a956f5f..000000000 --- a/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Emby.Dlna; -using Emby.Dlna.PlayTo; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; - -namespace Jellyfin.Dlna.Tests -{ - public class DlnaManagerTests - { - private DlnaManager GetManager() - { - var xmlSerializer = new Mock<IXmlSerializer>(); - var fileSystem = new Mock<IFileSystem>(); - var appPaths = new Mock<IApplicationPaths>(); - var loggerFactory = new Mock<ILoggerFactory>(); - var appHost = new Mock<IServerApplicationHost>(); - - return new DlnaManager(xmlSerializer.Object, fileSystem.Object, appPaths.Object, loggerFactory.Object, appHost.Object); - } - - [Fact] - public void IsMatch_GivenMatchingName_ReturnsTrue() - { - var device = new DeviceInfo() - { - Name = "My Device", - Manufacturer = "LG Electronics", - ManufacturerUrl = "http://www.lge.com", - ModelDescription = "LG WebOSTV DMRplus", - ModelName = "LG TV", - ModelNumber = "1.0", - }; - - var profile = new DeviceProfile() - { - Name = "Test Profile", - FriendlyName = "My Device", - Manufacturer = "LG Electronics", - ManufacturerUrl = "http://www.lge.com", - ModelDescription = "LG WebOSTV DMRplus", - ModelName = "LG TV", - ModelNumber = "1.0", - Identification = new() - { - FriendlyName = "My Device", - Manufacturer = "LG Electronics", - ManufacturerUrl = "http://www.lge.com", - ModelDescription = "LG WebOSTV DMRplus", - ModelName = "LG TV", - ModelNumber = "1.0", - } - }; - - var profile2 = new DeviceProfile() - { - Name = "Test Profile", - FriendlyName = "My Device", - Identification = new DeviceIdentification() - { - FriendlyName = "My Device", - } - }; - - var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile2.Identification); - var deviceMatch2 = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification); - - Assert.True(deviceMatch); - Assert.True(deviceMatch2); - } - - [Fact] - public void IsMatch_GivenNamesAndManufacturersDoNotMatch_ReturnsFalse() - { - var device = new DeviceInfo() - { - Name = "My Device", - Manufacturer = "JVC" - }; - - var profile = new DeviceProfile() - { - Name = "Test Profile", - FriendlyName = "My Device", - Manufacturer = "LG Electronics", - ManufacturerUrl = "http://www.lge.com", - ModelDescription = "LG WebOSTV DMRplus", - ModelName = "LG TV", - ModelNumber = "1.0", - Identification = new() - { - FriendlyName = "My Device", - Manufacturer = "LG Electronics", - ManufacturerUrl = "http://www.lge.com", - ModelDescription = "LG WebOSTV DMRplus", - ModelName = "LG TV", - ModelNumber = "1.0", - } - }; - - var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification); - - Assert.False(deviceMatch); - } - - [Fact] - public void IsMatch_GivenNamesAndRegExMatch_ReturnsTrue() - { - var device = new DeviceInfo() - { - Name = "My Device" - }; - - var profile = new DeviceProfile() - { - Name = "Test Profile", - FriendlyName = "My .*", - Identification = new() - }; - - var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification); - - Assert.True(deviceMatch); - } - } -} diff --git a/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs b/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs deleted file mode 100644 index 7655e3f7c..000000000 --- a/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Emby.Dlna.PlayTo; -using Xunit; - -namespace Jellyfin.Dlna.Tests -{ - public static class GetUuidTests - { - [Theory] - [InlineData("uuid:fc4ec57e-b051-11db-88f8-0060085db3f6::urn:schemas-upnp-org:device:WANDevice:1", "fc4ec57e-b051-11db-88f8-0060085db3f6")] - [InlineData("uuid:IGD{8c80f73f-4ba0-45fa-835d-042505d052be}000000000000", "8c80f73f-4ba0-45fa-835d-042505d052be")] - [InlineData("uuid:IGD{8c80f73f-4ba0-45fa-835d-042505d052be}000000000000::urn:schemas-upnp-org:device:InternetGatewayDevice:1", "8c80f73f-4ba0-45fa-835d-042505d052be")] - [InlineData("uuid:00000000-0000-0000-0000-000000000000::upnp:rootdevice", "00000000-0000-0000-0000-000000000000")] - [InlineData("uuid:fc4ec57e-b051-11db-88f8-0060085db3f6", "fc4ec57e-b051-11db-88f8-0060085db3f6")] - public static void GetUuid_Valid_Success(string usn, string uuid) - => Assert.Equal(uuid, PlayToManager.GetUuid(usn)); - } -} diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj deleted file mode 100644 index 69677ce42..000000000 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> - <PackageReference Include="xunit.runner.visualstudio"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="coverlet.collector" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" /> - </ItemGroup> - -</Project> diff --git a/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs deleted file mode 100644 index c9018fe2f..000000000 --- a/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Emby.Dlna.Server; -using MediaBrowser.Model.Dlna; -using Xunit; - -namespace Jellyfin.Dlna.Server.Tests; - -public class DescriptionXmlBuilderTests -{ - [Fact] - public void GetFriendlyName_EmptyProfile_ReturnsServerName() - { - const string ServerName = "Test Server Name"; - var builder = new DescriptionXmlBuilder(new DeviceProfile(), "serverUdn", "localhost", ServerName, string.Empty); - Assert.Equal(ServerName, builder.GetFriendlyName()); - } - - [Fact] - public void GetFriendlyName_FriendlyName_ReturnsFriendlyName() - { - const string FriendlyName = "Friendly Neighborhood Test Server"; - var builder = new DescriptionXmlBuilder( - new DeviceProfile() - { - FriendlyName = FriendlyName - }, - "serverUdn", - "localhost", - "Test Server Name", - string.Empty); - Assert.Equal(FriendlyName, builder.GetFriendlyName()); - } - - [Fact] - public void GetFriendlyName_FriendlyNameInterpolation_ReturnsFriendlyName() - { - var builder = new DescriptionXmlBuilder( - new DeviceProfile() - { - FriendlyName = "Friendly Neighborhood ${HostName}" - }, - "serverUdn", - "localhost", - "Test Server Name", - string.Empty); - Assert.Equal("Friendly Neighborhood TestServerName", builder.GetFriendlyName()); - } -} diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index 3747db3bb..2d7f11210 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -18,7 +18,7 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="../../Jellyfin.Networking/Jellyfin.Networking.csproj" /> + <ProjectReference Include="../../src/Jellyfin.Networking/Jellyfin.Networking.csproj" /> </ItemGroup> </Project> diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs deleted file mode 100644 index e5d5e785c..000000000 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http.Json; -using System.Net.Mime; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Jellyfin.Extensions.Json; -using MediaBrowser.Model.Dlna; -using Xunit; -using Xunit.Priority; - -namespace Jellyfin.Server.Integration.Tests.Controllers -{ - [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] - public sealed class DlnaControllerTests : IClassFixture<JellyfinApplicationFactory> - { - private const string NonExistentProfile = "1322f35b8f2c434dad3cc07c9b97dbd1"; - private readonly JellyfinApplicationFactory _factory; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private static string? _accessToken; - private static string? _newDeviceProfileId; - - public DlnaControllerTests(JellyfinApplicationFactory factory) - { - _factory = factory; - } - - [Fact] - [Priority(0)] - public async Task GetProfile_DoesNotExist_NotFound() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - [Priority(0)] - public async Task DeleteProfile_DoesNotExist_NotFound() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - [Priority(0)] - public async Task UpdateProfile_DoesNotExist_NotFound() - { - var client = _factory.CreateClient(); - 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); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - [Priority(1)] - public async Task CreateProfile_Valid_NoContent() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - var deviceProfile = new DeviceProfile() - { - Name = "ThisProfileIsNew" - }; - - using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - [Fact] - [Priority(2)] - public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - 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 response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); - - var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal)); - Assert.NotNull(newProfile); - _newDeviceProfileId = newProfile!.Id; - } - - [Fact] - [Priority(3)] - public async Task UpdateProfile_Valid_NoContent() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - var updatedProfile = new DeviceProfile() - { - Name = "ThisProfileIsUpdated", - Id = _newDeviceProfileId - }; - - 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"); - 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 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)); - Assert.NotNull(newProfile); - _newDeviceProfileId = newProfile!.Id; - } - - [Fact] - [Priority(5)] - public async Task DeleteProfile_Valid_NoContent() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - 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"); - 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 response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); - - Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal))); - } - } -} |
