Merge branch 'master' into tonemap

pull/3442/head
Nyanmisaka 4 years ago committed by GitHub
commit 4cb0a57e46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,10 +12,12 @@ parameters:
jobs:
- job: CompatibilityCheck
displayName: Compatibility Check
dependsOn: Build
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
pool:
vmImage: "${{ parameters.LinuxImage }}"
# only execute for pull requests
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
strategy:
matrix:
${{ each Package in parameters.Packages }}:
@ -23,7 +25,7 @@ jobs:
NugetPackageName: ${{ Package.value.NugetPackageName }}
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
maxParallel: 2
dependsOn: Build
steps:
- checkout: none
@ -34,32 +36,32 @@ jobs:
version: ${{ parameters.DotNetSdkVersion }}
- task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker tool'
displayName: 'Install ABI CompatibilityChecker Tool'
inputs:
command: custom
custom: tool
arguments: 'update compatibilitychecker -g'
- task: DownloadPipelineArtifact@2
displayName: "Download New Assembly Build Artifact"
displayName: 'Download New Assembly Build Artifact'
inputs:
source: "current"
source: 'current'
artifact: "$(NugetPackageName)"
path: "$(System.ArtifactsDirectory)/new-artifacts"
runVersion: "latest"
- task: CopyFiles@2
displayName: "Copy New Assembly Build Artifact"
displayName: 'Copy New Assembly Build Artifact'
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
contents: "**/*.dll"
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true
overWrite: true
flattenFolders: true
- task: DownloadPipelineArtifact@2
displayName: "Download Reference Assembly Build Artifact"
displayName: 'Download Reference Assembly Build Artifact'
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
@ -70,16 +72,15 @@ jobs:
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
- task: CopyFiles@2
displayName: "Copy Reference Assembly Build Artifact"
displayName: 'Copy Reference Assembly Build Artifact'
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: "**/*.dll"
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true
overWrite: true
flattenFolders: true
# The `--warnings-only` switch will swallow the return code and not emit any errors.
- task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool'
inputs:

@ -42,7 +42,7 @@ jobs:
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
displayName: 'Run Dockerfile (stable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
@ -87,7 +87,7 @@ jobs:
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- task: Docker@2
displayName: 'Push Unstable Image'
@ -104,7 +104,7 @@ jobs:
- task: Docker@2
displayName: 'Push Stable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
repository: 'jellyfin/jellyfin-server'
command: buildAndPush
@ -116,7 +116,9 @@ jobs:
$(JellyfinVersion)-$(BuildConfiguration)
- job: CollectArtifacts
timeoutInMinutes: 20
displayName: 'Collect Artifacts'
continueOnError: true
dependsOn:
- BuildPackage
- BuildDocker
@ -128,44 +130,85 @@ jobs:
steps:
- task: SSH@0
displayName: 'Update Unstable Repository'
continueOnError: true
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: |
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
rm $0
exit
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
displayName: 'Update Stable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
continueOnError: true
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: |
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
rm $0
exit
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NuGetCommand@2
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'pack'
packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
packDestination: '$(Build.ArtifactStagingDirectory)'
packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
versioningScheme: 'off'
- task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages'
inputs:
command: 'custom'
projects: |
Jellyfin.Data/Jellyfin.Data.csproj
MediaBrowser.Common/MediaBrowser.Common.csproj
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
- task: PublishBuildArtifacts@1
displayName: 'Publish Nuget packages'
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: Jellyfin Nuget Packages
- task: NuGetAuthenticate@0
displayName: 'Authenticate to stable Nuget feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
nuGetServiceConnections: 'NugetOrg'
- task: NuGetCommand@2
displayName: 'Push Nuget packages to stable feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
nuGetFeedType: 'external'
publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists
- task: NuGetAuthenticate@0
displayName: 'Authenticate to unstable Nuget feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- task: NuGetCommand@2
displayName: 'Push Nuget packages to unstable feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
includeNugetOrg: 'true'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
nuGetFeedType: 'internal'
publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
allowPackageConflicts: true # This ignores an error if the version already exists

@ -13,15 +13,21 @@ pr:
trigger:
batch: true
branches:
include:
- '*'
tags:
include:
- 'v*'
jobs:
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
- template: azure-pipelines-main.yml
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: $(RestoreBuildProjects)
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-test.yml
parameters:
ImageNames:
@ -29,7 +35,7 @@ jobs:
Windows: 'windows-latest'
macOS: 'macos-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml
parameters:
Packages:
@ -47,5 +53,5 @@ jobs:
AssemblyFileName: MediaBrowser.Common.dll
LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml

@ -6,11 +6,21 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
"console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": ".NET Core Launch (nowebclient)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"

@ -16,6 +16,7 @@
- [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator)
- [ckcr4lyf](https://github.com/ckcr4lyf)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [crankdoofus](https://github.com/crankdoofus)
- [crobibero](https://github.com/crobibero)
- [cromefire](https://github.com/cromefire)
@ -77,6 +78,7 @@
- [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka)
- [oddstr13](https://github.com/oddstr13)
- [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil)
- [Phlogi](https://github.com/Phlogi)
- [pjeanjean](https://github.com/pjeanjean)

@ -14,7 +14,7 @@ COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
FROM debian:buster-slim

@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-arm as qemu

@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/debian:buster-slim

@ -1,386 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Emby.Dlna.Main;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Dlna.Api
{
[Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")]
[Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")]
public class GetDescriptionXml
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
[Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
public class GetContentDirectory
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
[Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
public class GetConnnectionManager
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
[Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
public class GetMediaReceiverRegistrar
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")]
public class ProcessContentDirectoryControlRequest : IRequiresRequestStream
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
public Stream RequestStream { get; set; }
}
[Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")]
public class ProcessConnectionManagerControlRequest : IRequiresRequestStream
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
public Stream RequestStream { get; set; }
}
[Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")]
public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
public Stream RequestStream { get; set; }
}
[Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
[Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
public class ProcessMediaReceiverRegistrarEventRequest
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
[Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
public class ProcessContentDirectoryEventRequest
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
[Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
public class ProcessConnectionManagerEventRequest
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")]
[Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")]
public class GetIcon
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string UuId { get; set; }
[ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Filename { get; set; }
}
public class DlnaServerService : IService
{
private const string XMLContentType = "text/xml; charset=UTF-8";
private readonly IDlnaManager _dlnaManager;
private readonly IHttpResultFactory _resultFactory;
private readonly IServerConfigurationManager _configurationManager;
public IRequest Request { get; set; }
private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager;
private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
public DlnaServerService(
IDlnaManager dlnaManager,
IHttpResultFactory httpResultFactory,
IServerConfigurationManager configurationManager,
IHttpContextAccessor httpContextAccessor)
{
_dlnaManager = dlnaManager;
_resultFactory = httpResultFactory;
_configurationManager = configurationManager;
Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
private string GetHeader(string name)
{
return Request.Headers[name];
}
public object Get(GetDescriptionXml request)
{
var url = Request.AbsoluteUri;
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress);
var cacheLength = TimeSpan.FromDays(1);
var cacheKey = Request.RawUrl.GetMD5();
var bytes = Encoding.UTF8.GetBytes(xml);
return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes)));
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetContentDirectory request)
{
var xml = ContentDirectory.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetMediaReceiverRegistrar request)
{
var xml = MediaReceiverRegistrar.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetConnnectionManager request)
{
var xml = ConnectionManager.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
{
var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
public async Task<object> Post(ProcessContentDirectoryControlRequest request)
{
var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
public async Task<object> Post(ProcessConnectionManagerControlRequest request)
{
var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
{
var id = GetPathValue(2).ToString();
return service.ProcessControlRequestAsync(new ControlRequest
{
Headers = Request.Headers,
InputXml = requestStream,
TargetServerUuId = id,
RequestedUrl = Request.AbsoluteUri
});
}
// Copied from MediaBrowser.Api/BaseApiService.cs
// TODO: Remove code duplication
/// <summary>
/// Gets the path segment at the specified index.
/// </summary>
/// <param name="index">The index of the path segment.</param>
/// <returns>The path segment at the specified index.</returns>
/// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
/// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
protected internal ReadOnlySpan<char> GetPathValue(int index)
{
static void ThrowIndexOutOfRangeException()
=> throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
static void ThrowInvalidDataException()
=> throw new InvalidDataException("Path doesn't start with the base url.");
ReadOnlySpan<char> path = Request.PathInfo;
// Remove the protocol part from the url
int pos = path.LastIndexOf("://");
if (pos != -1)
{
path = path.Slice(pos + 3);
}
// Remove the query string
pos = path.LastIndexOf('?');
if (pos != -1)
{
path = path.Slice(0, pos);
}
// Remove the domain
pos = path.IndexOf('/');
if (pos != -1)
{
path = path.Slice(pos);
}
// Remove base url
string baseUrl = _configurationManager.Configuration.BaseUrl;
int baseUrlLen = baseUrl.Length;
if (baseUrlLen != 0)
{
if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
{
path = path.Slice(baseUrlLen);
}
else
{
// The path doesn't start with the base url,
// how did we get here?
ThrowInvalidDataException();
}
}
// Remove leading /
path = path.Slice(1);
// Backwards compatibility
const string Emby = "emby/";
if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
{
path = path.Slice(Emby.Length);
}
const string MediaBrowser = "mediabrowser/";
if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
{
path = path.Slice(MediaBrowser.Length);
}
// Skip segments until we are at the right index
for (int i = 0; i < index; i++)
{
pos = path.IndexOf('/');
if (pos == -1)
{
ThrowIndexOutOfRangeException();
}
path = path.Slice(pos + 1);
}
// Remove the rest
pos = path.IndexOf('/');
if (pos != -1)
{
path = path.Slice(0, pos);
}
return path;
}
public object Get(GetIcon request)
{
var contentType = "image/" + Path.GetExtension(request.Filename)
.TrimStart('.')
.ToLowerInvariant();
var cacheLength = TimeSpan.FromDays(365);
var cacheKey = Request.RawUrl.GetMD5();
return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream));
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Subscribe(ProcessContentDirectoryEventRequest request)
{
return ProcessEventRequest(ContentDirectory);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Subscribe(ProcessConnectionManagerEventRequest request)
{
return ProcessEventRequest(ConnectionManager);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
{
return ProcessEventRequest(MediaReceiverRegistrar);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Unsubscribe(ProcessContentDirectoryEventRequest request)
{
return ProcessEventRequest(ContentDirectory);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Unsubscribe(ProcessConnectionManagerEventRequest request)
{
return ProcessEventRequest(ConnectionManager);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request)
{
return ProcessEventRequest(MediaReceiverRegistrar);
}
private object ProcessEventRequest(IEventManager eventManager)
{
var subscriptionId = GetHeader("SID");
if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase))
{
var notificationType = GetHeader("NT");
var callback = GetHeader("CALLBACK");
var timeoutString = GetHeader("TIMEOUT");
if (string.IsNullOrEmpty(notificationType))
{
return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback));
}
return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback));
}
return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId));
}
private object GetSubscriptionResponse(EventSubscriptionResponse response)
{
return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers);
}
}
}

@ -1,88 +0,0 @@
#pragma warning disable CS1591
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Services;
namespace Emby.Dlna.Api
{
[Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
public class GetProfileInfos : IReturn<DeviceProfileInfo[]>
{
}
[Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")]
public class DeleteProfile : IReturnVoid
{
[ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
public string Id { get; set; }
}
[Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")]
public class GetDefaultProfile : IReturn<DeviceProfile>
{
}
[Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")]
public class GetProfile : IReturn<DeviceProfile>
{
[ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Id { get; set; }
}
[Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")]
public class UpdateProfile : DeviceProfile, IReturnVoid
{
}
[Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")]
public class CreateProfile : DeviceProfile, IReturnVoid
{
}
[Authenticated(Roles = "Admin")]
public class DlnaService : IService
{
private readonly IDlnaManager _dlnaManager;
public DlnaService(IDlnaManager dlnaManager)
{
_dlnaManager = dlnaManager;
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetProfileInfos request)
{
return _dlnaManager.GetProfileInfos().ToArray();
}
public object Get(GetProfile request)
{
return _dlnaManager.GetProfile(request.Id);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetDefaultProfile request)
{
return _dlnaManager.GetDefaultProfile();
}
public void Delete(DeleteProfile request)
{
_dlnaManager.DeleteProfile(request.Id);
}
public void Post(UpdateProfile request)
{
_dlnaManager.UpdateProfile(request);
}
public void Post(CreateProfile request)
{
_dlnaManager.CreateProfile(request);
}
}
}

@ -13,7 +13,7 @@ namespace Emby.Dlna.Common
public string Name { get; set; }
public List<Argument> ArgumentList { get; set; }
public List<Argument> ArgumentList { get; }
/// <inheritdoc />
public override string ToString()

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
@ -17,7 +18,7 @@ namespace Emby.Dlna.Common
public bool SendsEvents { get; set; }
public string[] AllowedValues { get; set; }
public IReadOnlyList<string> AllowedValues { get; set; }
/// <inheritdoc />
public override string ToString()

@ -1,7 +1,6 @@
#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Configuration;
using MediaBrowser.Common.Configuration;
@ -14,19 +13,4 @@ namespace Emby.Dlna
return manager.GetConfiguration<DlnaOptions>("dlna");
}
}
public class DlnaConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
Key = "dlna",
ConfigurationType = typeof (DlnaOptions)
}
};
}
}
}

@ -9,22 +9,20 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
public class ConnectionManager : BaseService, IConnectionManager
public class ConnectionManagerService : BaseService, IConnectionManager
{
private readonly IDlnaManager _dlna;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
public ConnectionManager(
public ConnectionManagerService(
IDlnaManager dlna,
IServerConfigurationManager config,
ILogger<ConnectionManager> logger,
ILogger<ConnectionManagerService> logger,
IHttpClient httpClient)
: base(logger, httpClient)
{
_dlna = dlna;
_config = config;
_logger = logger;
}
/// <inheritdoc />
@ -39,7 +37,7 @@ namespace Emby.Dlna.ConnectionManager
var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile();
return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
}
}
}

@ -44,7 +44,7 @@ namespace Emby.Dlna.ConnectionManager
DataType = "string",
SendsEvents = false,
AllowedValues = new string[]
AllowedValues = new[]
{
"OK",
"ContentFormatMismatch",
@ -67,7 +67,7 @@ namespace Emby.Dlna.ConnectionManager
DataType = "string",
SendsEvents = false,
AllowedValues = new string[]
AllowedValues = new[]
{
"Output",
"Input"

@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory
{
public class ContentDirectory : BaseService, IContentDirectory
public class ContentDirectoryService : BaseService, IContentDirectory
{
private readonly ILibraryManager _libraryManager;
private readonly IImageProcessor _imageProcessor;
@ -33,14 +33,14 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager;
public ContentDirectory(
public ContentDirectoryService(
IDlnaManager dlna,
IUserDataManager userDataManager,
IImageProcessor imageProcessor,
ILibraryManager libraryManager,
IServerConfigurationManager config,
IUserManager userManager,
ILogger<ContentDirectory> logger,
ILogger<ContentDirectoryService> logger,
IHttpClient httpClient,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,

@ -10,7 +10,8 @@ namespace Emby.Dlna.ContentDirectory
{
public string GetXml()
{
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables());
}
@ -101,7 +102,7 @@ namespace Emby.Dlna.ContentDirectory
DataType = "string",
SendsEvents = false,
AllowedValues = new string[]
AllowedValues = new[]
{
"BrowseMetadata",
"BrowseDirectChildren"

@ -40,6 +40,11 @@ namespace Emby.Dlna.ContentDirectory
{
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 IServerConfigurationManager _config;
@ -47,11 +52,6 @@ namespace Emby.Dlna.ContentDirectory
private readonly IUserViewManager _userViewManager;
private readonly ITVSeriesManager _tvSeriesManager;
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private readonly int _systemUpdateId;
private readonly DidlBuilder _didlBuilder;
@ -181,7 +181,11 @@ namespace Emby.Dlna.ContentDirectory
userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
_userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed,
_userDataManager.SaveUserData(
_user,
item,
userdata,
UserDataSaveReason.TogglePlayed,
CancellationToken.None);
}
@ -253,7 +257,7 @@ namespace Emby.Dlna.ContentDirectory
var id = sparams["ObjectID"];
var flag = sparams["BrowseFlag"];
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var provided = 0;
@ -286,18 +290,17 @@ namespace Emby.Dlna.ContentDirectory
using (var writer = XmlWriter.Create(builder, settings))
{
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
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;
@ -362,8 +365,8 @@ namespace Emby.Dlna.ContentDirectory
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
{
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", ""));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
// sort example: dc:title, dc:date
@ -397,11 +400,11 @@ namespace Emby.Dlna.ContentDirectory
using (var writer = XmlWriter.Create(builder, settings))
{
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
DidlBuilder.WriteXmlRootAttributes(_profile, writer);
@ -783,11 +786,14 @@ namespace Emby.Dlna.ContentDirectory
})
.ToArray();
return ApplyPaging(new QueryResult<ServerItem>
{
Items = folders,
TotalRecordCount = folders.Length
}, startIndex, limit);
return ApplyPaging(
new QueryResult<ServerItem>
{
Items = folders,
TotalRecordCount = folders.Length
},
startIndex,
limit);
}
private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
@ -1135,14 +1141,16 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { nameof(Audio) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { nameof(Audio) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
}
@ -1151,12 +1159,15 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var result = _tvSeriesManager.GetNextUp(new NextUpQuery
{
Limit = query.Limit,
StartIndex = query.StartIndex,
UserId = query.User.Id
}, new[] { parent }, query.DtoOptions);
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
{
Limit = query.Limit,
StartIndex = query.StartIndex,
UserId = query.User.Id
},
new[] { parent },
query.DtoOptions);
return ToResult(result);
}
@ -1165,14 +1176,16 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { typeof(Episode).Name },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { typeof(Episode).Name },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
}
@ -1183,13 +1196,14 @@ namespace Emby.Dlna.ContentDirectory
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { nameof(Movie) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { nameof(Movie) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
}
@ -1349,49 +1363,9 @@ namespace Emby.Dlna.ContentDirectory
};
}
Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id);
Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
return new ServerItem(_libraryManager.GetUserRootFolder());
}
}
internal class ServerItem
{
public BaseItem Item { get; set; }
public StubType? StubType { get; set; }
public ServerItem(BaseItem item)
{
Item = item;
if (item is IItemByName && !(item is Folder))
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
}
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
}
}

@ -0,0 +1,23 @@
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
{
internal class ServerItem
{
public ServerItem(BaseItem item)
{
Item = item;
if (item is IItemByName && !(item is Folder))
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
public BaseItem Item { get; set; }
public StubType? StubType { get; set; }
}
}

@ -0,0 +1,28 @@
#pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory
{
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
}
}

@ -7,17 +7,17 @@ namespace Emby.Dlna
{
public class ControlRequest
{
public IHeaderDictionary Headers { get; set; }
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; }
public ControlRequest()
{
Headers = new HeaderDictionary();
}
}
}

@ -11,10 +11,16 @@ namespace Emby.Dlna
Headers = new Dictionary<string, string>();
}
public IDictionary<string, string> Headers { get; set; }
public IDictionary<string, string> Headers { get; }
public string Xml { get; set; }
public bool IsSuccessful { get; set; }
/// <inheritdoc />
public override string ToString()
{
return Xml;
}
}
}

@ -34,12 +34,12 @@ namespace Emby.Dlna.Didl
{
public class DidlBuilder
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
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 const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
@ -100,11 +100,11 @@ namespace Emby.Dlna.Didl
{
// writer.WriteStartDocument();
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
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);
@ -147,7 +147,7 @@ namespace Emby.Dlna.Didl
{
var clientId = GetClientId(item, null);
writer.WriteStartElement(string.Empty, "item", NS_DIDL);
writer.WriteStartElement(string.Empty, "item", NsDidl);
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("id", clientId);
@ -207,7 +207,8 @@ namespace Emby.Dlna.Didl
var targetWidth = streamInfo.TargetWidth;
var targetHeight = streamInfo.TargetHeight;
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetWidth,
@ -279,7 +280,7 @@ namespace Emby.Dlna.Didl
}
else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
{
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
@ -288,7 +289,7 @@ namespace Emby.Dlna.Didl
}
else
{
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
var protocolInfo = string.Format(
CultureInfo.InvariantCulture,
"http-get:*:text/{0}:*",
@ -304,7 +305,7 @@ namespace Emby.Dlna.Didl
private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
{
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
@ -526,7 +527,7 @@ namespace Emby.Dlna.Didl
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
{
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
if (streamInfo == null)
{
@ -583,7 +584,8 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
}
var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container,
var mediaProfile = _profile.GetAudioMediaProfile(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetChannels,
targetAudioBitrate,
@ -596,7 +598,8 @@ namespace Emby.Dlna.Didl
? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType;
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetAudioBitrate,
targetSampleRate,
@ -627,7 +630,7 @@ namespace Emby.Dlna.Didl
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
{
writer.WriteStartElement(string.Empty, "container", NS_DIDL);
writer.WriteStartElement(string.Empty, "container", NsDidl);
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1");
@ -714,7 +717,7 @@ namespace Emby.Dlna.Didl
// MediaMonkey for example won't display content without a title
// if (filter.Contains("dc:title"))
{
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc);
}
WriteObjectClass(writer, item, itemStubType);
@ -723,7 +726,7 @@ namespace Emby.Dlna.Didl
{
if (item.PremiereDate.HasValue)
{
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC);
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
}
}
@ -731,13 +734,13 @@ namespace Emby.Dlna.Didl
{
foreach (var genre in item.Genres)
{
AddValue(writer, "upnp", "genre", genre, NS_UPNP);
AddValue(writer, "upnp", "genre", genre, NsUpnp);
}
}
foreach (var studio in item.Studios)
{
AddValue(writer, "upnp", "publisher", studio, NS_UPNP);
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
}
if (!(item is Folder))
@ -748,28 +751,29 @@ namespace Emby.Dlna.Didl
if (!string.IsNullOrWhiteSpace(desc))
{
AddValue(writer, "dc", "description", desc, NS_DC);
AddValue(writer, "dc", "description", desc, NsDc);
}
}
// if (filter.Contains("upnp:longDescription"))
//{
// {
// if (!string.IsNullOrWhiteSpace(item.Overview))
// {
// AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP);
// AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp);
// }
//}
// }
}
if (!string.IsNullOrEmpty(item.OfficialRating))
{
if (filter.Contains("dc:rating"))
{
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
AddValue(writer, "dc", "rating", item.OfficialRating, NsDc);
}
if (filter.Contains("upnp:rating"))
{
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
}
}
@ -781,7 +785,7 @@ namespace Emby.Dlna.Didl
// More types here
// http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
writer.WriteStartElement("upnp", "class", NS_UPNP);
writer.WriteStartElement("upnp", "class", NsUpnp);
if (item.IsDisplayedAsFolder || stubType.HasValue)
{
@ -882,7 +886,7 @@ namespace Emby.Dlna.Didl
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
?? PersonType.Actor;
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP);
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
}
}
@ -896,8 +900,8 @@ namespace Emby.Dlna.Didl
{
foreach (var artist in hasArtists.Artists)
{
AddValue(writer, "upnp", "artist", artist, NS_UPNP);
AddValue(writer, "dc", "creator", artist, NS_DC);
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 == null)
@ -917,16 +921,16 @@ namespace Emby.Dlna.Didl
if (!string.IsNullOrWhiteSpace(item.Album))
{
AddValue(writer, "upnp", "album", item.Album, NS_UPNP);
AddValue(writer, "upnp", "album", item.Album, NsUpnp);
}
if (item.IndexNumber.HasValue)
{
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
if (item is Episode)
{
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
}
}
}
@ -935,7 +939,7 @@ namespace Emby.Dlna.Didl
{
try
{
writer.WriteStartElement("upnp", "artist", NS_UPNP);
writer.WriteStartElement("upnp", "artist", NsUpnp);
writer.WriteAttributeString("role", "AlbumArtist");
writer.WriteString(name);
@ -944,7 +948,7 @@ namespace Emby.Dlna.Didl
}
catch (XmlException ex)
{
_logger.LogError(ex, "Error adding xml value: {value}", name);
_logger.LogError(ex, "Error adding xml value: {Value}", name);
}
}
@ -956,7 +960,7 @@ namespace Emby.Dlna.Didl
}
catch (XmlException ex)
{
_logger.LogError(ex, "Error adding xml value: {value}", value);
_logger.LogError(ex, "Error adding xml value: {Value}", value);
}
}
@ -971,14 +975,14 @@ namespace Emby.Dlna.Didl
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP);
writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn);
writer.WriteString(albumartUrlInfo.Url);
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
writer.WriteString(albumartUrlInfo.url);
writer.WriteFullEndElement();
// TOOD: Remove these default values
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url);
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
if (!_profile.EnableAlbumArtInDidl)
{
@ -1021,12 +1025,12 @@ namespace Emby.Dlna.Didl
var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
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 width = albumartUrlInfo.width ?? maxWidth;
var height = albumartUrlInfo.height ?? maxHeight;
var contentFeatures = new ContentFeatureBuilder(_profile)
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
@ -1043,7 +1047,7 @@ namespace Emby.Dlna.Didl
"resolution",
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
writer.WriteString(albumartUrlInfo.Url);
writer.WriteString(albumartUrlInfo.url);
writer.WriteFullEndElement();
}
@ -1139,7 +1143,6 @@ namespace Emby.Dlna.Didl
if (width == 0 || height == 0)
{
// _imageProcessor.GetImageSize(item, imageInfo);
width = null;
height = null;
}
@ -1149,18 +1152,6 @@ namespace Emby.Dlna.Didl
height = null;
}
// try
//{
// var size = _imageProcessor.GetImageSize(imageInfo);
// width = size.Width;
// height = size.Height;
//}
// catch
//{
//}
var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
.TrimStart('.')
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
@ -1177,30 +1168,6 @@ namespace Emby.Dlna.Didl
};
}
private class ImageDownloadInfo
{
internal Guid ItemId;
internal string ImageTag;
internal ImageType Type;
internal int? Width;
internal int? Height;
internal bool IsDirectStream;
internal string Format;
internal ItemImageInfo ItemImageInfo;
}
private class ImageUrlInfo
{
internal string Url;
internal int? Width;
internal int? Height;
}
public static string GetClientId(BaseItem item, StubType? stubType)
{
return GetClientId(item.Id, stubType);
@ -1218,7 +1185,7 @@ namespace Emby.Dlna.Didl
return id;
}
private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
{
var url = string.Format(
CultureInfo.InvariantCulture,
@ -1256,12 +1223,26 @@ namespace Emby.Dlna.Didl
// just lie
info.IsDirectStream = true;
return new ImageUrlInfo
{
Url = url,
Width = width,
Height = height
};
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 string Format { get; set; }
internal ItemImageInfo ItemImageInfo { get; set; }
}
}
}

@ -23,9 +23,7 @@ namespace Emby.Dlna.Didl
public bool Contains(string field)
{
// Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back.
return true;
// return _all || ListHelper.ContainsIgnoreCase(_fields, field);
return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
}
}
}

@ -1,4 +1,5 @@
#pragma warning disable CS1591
#pragma warning disable CA1305
using System;
using System.IO;
@ -29,7 +30,6 @@ namespace Emby.Dlna.Didl
{
}
public StringWriterWithEncoding(Encoding encoding)
{
_encoding = encoding;

@ -0,0 +1,24 @@
#nullable enable
#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)
}
};
}
}
}

@ -54,11 +54,15 @@ namespace Emby.Dlna
_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();
await ExtractSystemProfilesAsync().ConfigureAwait(false);
LoadProfiles();
}
catch (Exception ex)
@ -240,7 +244,7 @@ namespace Emby.Dlna
}
else
{
var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
_logger.LogDebug("No matching device profile found. {0}", headerString);
}
@ -280,10 +284,6 @@ namespace Emby.Dlna
return false;
}
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
{
try
@ -495,8 +495,8 @@ namespace Emby.Dlna
/// Recreates the object using serialization, to ensure it's not a subclass.
/// If it's a subclass it may not serlialize properly to xml (different root element tag name).
/// </summary>
/// <param name="profile"></param>
/// <returns></returns>
/// <param name="profile">The device profile.</param>
/// <returns>The reserialized device profile.</returns>
private DeviceProfile ReserializeProfile(DeviceProfile profile)
{
if (profile.GetType() == typeof(DeviceProfile))
@ -509,13 +509,6 @@ namespace Emby.Dlna
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
}
private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
}
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetProfile(headers) ??
@ -540,7 +533,15 @@ namespace Emby.Dlna
Stream = _assembly.GetManifestResourceStream(resource)
};
}
private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
}
}
/*
class DlnaProfileEntryPoint : IServerEntryPoint
{

@ -20,7 +20,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->

@ -15,6 +15,6 @@ namespace Emby.Dlna
public string ContentType { get; set; }
public Dictionary<string, string> Headers { get; set; }
public Dictionary<string, string> Headers { get; }
}
}

@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Eventing
{
public class EventManager : IEventManager
public class DlnaEventManager : IDlnaEventManager
{
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
@ -22,7 +22,9 @@ namespace Emby.Dlna.Eventing
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
public EventManager(ILogger logger, IHttpClient httpClient)
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public DlnaEventManager(ILogger logger, IHttpClient httpClient)
{
_httpClient = httpClient;
_logger = logger;
@ -58,7 +60,8 @@ namespace Emby.Dlna.Eventing
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}",
_logger.LogDebug(
"Creating event subscription for {0} with timeout of {1} to {2}",
notificationType,
timeout,
callbackUrl);
@ -94,7 +97,7 @@ namespace Emby.Dlna.Eventing
{
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
_subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
_subscriptions.TryRemove(subscriptionId, out _);
return new EventSubscriptionResponse
{
@ -103,7 +106,6 @@ namespace Emby.Dlna.Eventing
};
}
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
{
var response = new EventSubscriptionResponse

@ -2,7 +2,7 @@
namespace Emby.Dlna
{
public interface IConnectionManager : IEventManager, IUpnpService
public interface IConnectionManager : IDlnaEventManager, IUpnpService
{
}
}

@ -2,7 +2,7 @@
namespace Emby.Dlna
{
public interface IContentDirectory : IEventManager, IUpnpService
public interface IContentDirectory : IDlnaEventManager, IUpnpService
{
}
}

@ -2,22 +2,32 @@
namespace Emby.Dlna
{
public interface IEventManager
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 sting.</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 sting.</param>
/// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
}
}

@ -2,7 +2,7 @@
namespace Emby.Dlna
{
public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
{
}
}

@ -30,7 +30,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main
{
public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
{
private readonly IServerConfigurationManager _config;
private readonly ILogger<DlnaEntryPoint> _logger;
@ -54,13 +54,7 @@ namespace Emby.Dlna.Main
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer;
internal IContentDirectory ContentDirectory { get; private set; }
internal IConnectionManager ConnectionManager { get; private set; }
internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public static DlnaEntryPoint Current;
private bool _disposed;
public DlnaEntryPoint(
IServerConfigurationManager config,
@ -99,14 +93,14 @@ namespace Emby.Dlna.Main
_networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
ContentDirectory = new ContentDirectory.ContentDirectory(
ContentDirectory = new ContentDirectory.ContentDirectoryService(
dlnaManager,
userDataManager,
imageProcessor,
libraryManager,
config,
userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(),
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
httpClient,
localizationManager,
mediaSourceManager,
@ -114,19 +108,27 @@ namespace Emby.Dlna.Main
mediaEncoder,
tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManager(
ConnectionManager = new ConnectionManager.ConnectionManagerService(
dlnaManager,
config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(),
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClient);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(),
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClient,
config);
Current = this;
}
public static DlnaEntryPoint Current { get; private set; }
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public async Task RunAsync()
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@ -399,8 +401,24 @@ namespace Emby.Dlna.Main
}
}
public void DisposeDevicePublisher()
{
if (_publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
DisposeDevicePublisher();
DisposePlayToManager();
DisposeDeviceDiscovery();
@ -416,16 +434,8 @@ namespace Emby.Dlna.Main
ConnectionManager = null;
MediaReceiverRegistrar = null;
Current = null;
}
public void DisposeDevicePublisher()
{
if (_publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
_disposed = true;
}
}
}

@ -8,12 +8,12 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
{
private readonly IServerConfigurationManager _config;
public MediaReceiverRegistrar(
ILogger<MediaReceiverRegistrar> logger,
public MediaReceiverRegistrarService(
ILogger<MediaReceiverRegistrarService> logger,
IHttpClient httpClient,
IServerConfigurationManager config)
: base(logger, httpClient)

@ -10,7 +10,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar
{
public string GetXml()
{
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables());
}

@ -19,15 +19,40 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private readonly IHttpClient _httpClient;
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, IHttpClient httpClient, ILogger logger)
{
Properties = deviceProperties;
_httpClient = httpClient;
_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; }
private int _muteVol;
public bool IsMuted { get; set; }
private int _volume;
public int Volume
{
get
@ -43,29 +68,21 @@ namespace Emby.Dlna.PlayTo
public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
public TRANSPORTSTATE TransportState { get; private set; }
public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING;
public TransportState TransportState { get; private set; }
public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
public bool IsPlaying => TransportState == TransportState.Playing;
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
private readonly IHttpClient _httpClient;
public bool IsStopped => TransportState == TransportState.Stopped;
private readonly ILogger _logger;
public Action OnDeviceUnavailable { get; set; }
private readonly IServerConfigurationManager _config;
private TransportCommands AvCommands { get; set; }
public Action OnDeviceUnavailable { get; set; }
private TransportCommands RendererCommands { get; set; }
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
{
Properties = deviceProperties;
_httpClient = httpClient;
_logger = logger;
_config = config;
}
public UBaseObject CurrentMediaInfo { get; private set; }
public void Start()
{
@ -73,8 +90,6 @@ namespace Emby.Dlna.PlayTo
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
}
private DateTime _lastVolumeRefresh;
private bool _volumeRefreshActive;
private Task RefreshVolumeIfNeeded()
{
if (_volumeRefreshActive
@ -105,7 +120,6 @@ namespace Emby.Dlna.PlayTo
}
}
private readonly object _timerLock = new object();
private void RestartTimer(bool immediate = false)
{
lock (_timerLock)
@ -233,6 +247,9 @@ namespace Emby.Dlna.PlayTo
/// <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);
@ -275,7 +292,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
.ConfigureAwait(false);
RestartTimer(true);
@ -285,7 +302,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
url = url.Replace("&", "&amp;");
url = url.Replace("&", "&amp;", StringComparison.Ordinal);
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
@ -297,8 +314,8 @@ namespace Emby.Dlna.PlayTo
var dictionary = new Dictionary<string, string>
{
{"CurrentURI", url},
{"CurrentURIMetaData", CreateDidlMeta(metaData)}
{ "CurrentURI", url },
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
};
var service = GetAvTransportService();
@ -401,13 +418,11 @@ namespace Emby.Dlna.PlayTo
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
.ConfigureAwait(false);
TransportState = TRANSPORTSTATE.PAUSED;
TransportState = TransportState.Paused;
RestartTimer(true);
}
private int _connectFailureCount;
private async void TimerCallback(object sender)
{
if (_disposed)
@ -436,7 +451,7 @@ namespace Emby.Dlna.PlayTo
if (transportState.HasValue)
{
// If we're not playing anything no need to get additional data
if (transportState.Value == TRANSPORTSTATE.STOPPED)
if (transportState.Value == TransportState.Stopped)
{
UpdateMediaInfo(null, transportState.Value);
}
@ -465,7 +480,7 @@ namespace Emby.Dlna.PlayTo
}
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
if (transportState.Value == TRANSPORTSTATE.STOPPED)
if (transportState.Value == TransportState.Stopped)
{
RestartTimerInactive();
}
@ -539,7 +554,7 @@ namespace Emby.Dlna.PlayTo
return;
}
var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
var volumeValue = volume?.Value;
if (string.IsNullOrWhiteSpace(volumeValue))
@ -589,14 +604,14 @@ namespace Emby.Dlna.PlayTo
return;
}
var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
.Select(i => i.Element("CurrentMute"))
.FirstOrDefault(i => i != null);
IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
}
private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
if (command == null)
@ -623,12 +638,12 @@ namespace Emby.Dlna.PlayTo
}
var transportState =
result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
var transportStateValue = transportState?.Value;
if (transportStateValue != null
&& Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
&& Enum.TryParse(transportStateValue, true, out TransportState state))
{
return state;
}
@ -636,7 +651,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command == null)
@ -671,7 +686,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
var e = track.Element(uPnpNamespaces.items) ?? track;
var e = track.Element(UPnpNamespaces.Items) ?? track;
var elementString = (string)e;
@ -687,13 +702,13 @@ namespace Emby.Dlna.PlayTo
return null;
}
e = track.Element(uPnpNamespaces.items) ?? track;
e = track.Element(UPnpNamespaces.Items) ?? track;
elementString = (string)e;
if (!string.IsNullOrWhiteSpace(elementString))
{
return new uBaseObject
return new UBaseObject
{
Url = elementString
};
@ -702,7 +717,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command == null)
@ -731,11 +746,11 @@ namespace Emby.Dlna.PlayTo
return (false, null);
}
var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
var trackUri = trackUriElem == null ? null : trackUriElem.Value;
var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
var trackUri = trackUriElem?.Value;
var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
var duration = durationElem == null ? null : durationElem.Value;
var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
var duration = durationElem?.Value;
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@ -747,8 +762,8 @@ namespace Emby.Dlna.PlayTo
Duration = null;
}
var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
var position = positionElem == null ? null : positionElem.Value;
var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
var position = positionElem?.Value;
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
@ -787,7 +802,7 @@ namespace Emby.Dlna.PlayTo
return (true, null);
}
var e = uPnpResponse.Element(uPnpNamespaces.items);
var e = uPnpResponse.Element(UPnpNamespaces.Items);
var uTrack = CreateUBaseObject(e, trackUri);
@ -819,7 +834,7 @@ namespace Emby.Dlna.PlayTo
// some devices send back invalid xml
try
{
return XElement.Parse(xml.Replace("&", "&amp;"));
return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
}
catch (XmlException)
{
@ -828,27 +843,27 @@ namespace Emby.Dlna.PlayTo
return null;
}
private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
var url = container.GetValue(uPnpNamespaces.Res);
var url = container.GetValue(UPnpNamespaces.Res);
if (string.IsNullOrWhiteSpace(url))
{
url = trackUri;
}
return new uBaseObject
return new UBaseObject
{
Id = container.GetAttributeValue(uPnpNamespaces.Id),
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
Title = container.GetValue(uPnpNamespaces.title),
IconUrl = container.GetValue(uPnpNamespaces.Artwork),
SecondText = "",
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()
@ -862,11 +877,11 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(container));
}
var resElement = container.Element(uPnpNamespaces.Res);
var resElement = container.Element(UPnpNamespaces.Res);
if (resElement != null)
{
var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
if (info != null && !string.IsNullOrWhiteSpace(info.Value))
{
@ -941,12 +956,12 @@ namespace Emby.Dlna.PlayTo
return url;
}
if (!url.Contains("/"))
if (!url.Contains('/', StringComparison.Ordinal))
{
url = "/dmr/" + url;
}
if (!url.StartsWith("/"))
if (!url.StartsWith("/", StringComparison.Ordinal))
{
url = "/" + url;
}
@ -954,11 +969,7 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url;
}
private TransportCommands AvCommands { get; set; }
private TransportCommands RendererCommands { get; set; }
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
{
var ssdpHttpClient = new SsdpHttpClient(httpClient);
@ -966,13 +977,13 @@ namespace Emby.Dlna.PlayTo
var friendlyNames = new List<string>();
var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
if (name != null && !string.IsNullOrWhiteSpace(name.Value))
{
friendlyNames.Add(name.Value);
}
var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
if (room != null && !string.IsNullOrWhiteSpace(room.Value))
{
friendlyNames.Add(room.Value);
@ -981,77 +992,77 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo()
{
Name = string.Join(" ", friendlyNames),
BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
};
var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
if (model != null)
{
deviceProperties.ModelName = model.Value;
}
var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
if (modelNumber != null)
{
deviceProperties.ModelNumber = modelNumber.Value;
}
var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
if (uuid != null)
{
deviceProperties.UUID = uuid.Value;
}
var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
if (manufacturer != null)
{
deviceProperties.Manufacturer = manufacturer.Value;
}
var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
if (manufacturerUrl != null)
{
deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
}
var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
if (presentationUrl != null)
{
deviceProperties.PresentationUrl = presentationUrl.Value;
}
var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
if (modelUrl != null)
{
deviceProperties.ModelUrl = modelUrl.Value;
}
var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
if (serialNumber != null)
{
deviceProperties.SerialNumber = serialNumber.Value;
}
var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
if (modelDescription != null)
{
deviceProperties.ModelDescription = modelDescription.Value;
}
var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
if (icon != null)
{
deviceProperties.Icon = CreateIcon(icon);
}
foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
{
if (services == null)
{
continue;
}
var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
if (servicesList == null)
{
continue;
@ -1068,10 +1079,9 @@ namespace Emby.Dlna.PlayTo
}
}
return new Device(deviceProperties, httpClient, logger, config);
return new Device(deviceProperties, httpClient, logger);
}
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private static DeviceIcon CreateIcon(XElement element)
{
if (element == null)
@ -1079,11 +1089,11 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(element));
}
var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype"));
var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width"));
var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height"));
var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth"));
var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url"));
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
@ -1100,11 +1110,11 @@ namespace Emby.Dlna.PlayTo
private static DeviceService Create(XElement element)
{
var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType"));
var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId"));
var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL"));
var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
return new DeviceService
{
@ -1116,14 +1126,7 @@ namespace Emby.Dlna.PlayTo
};
}
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
public event EventHandler<MediaChangedEventArgs> MediaChanged;
public uBaseObject CurrentMediaInfo { get; private set; }
private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
{
TransportState = state;
@ -1132,7 +1135,7 @@ namespace Emby.Dlna.PlayTo
if (previousMediaInfo == null && mediaInfo != null)
{
if (state != TRANSPORTSTATE.STOPPED)
if (state != TransportState.Stopped)
{
OnPlaybackStart(mediaInfo);
}
@ -1151,7 +1154,7 @@ namespace Emby.Dlna.PlayTo
}
}
private void OnPlaybackStart(uBaseObject mediaInfo)
private void OnPlaybackStart(UBaseObject mediaInfo)
{
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{
@ -1164,7 +1167,7 @@ namespace Emby.Dlna.PlayTo
});
}
private void OnPlaybackProgress(uBaseObject mediaInfo)
private void OnPlaybackProgress(UBaseObject mediaInfo)
{
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{
@ -1177,7 +1180,7 @@ namespace Emby.Dlna.PlayTo
});
}
private void OnPlaybackStop(uBaseObject mediaInfo)
private void OnPlaybackStop(UBaseObject mediaInfo)
{
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
{
@ -1185,7 +1188,7 @@ namespace Emby.Dlna.PlayTo
});
}
private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
{
MediaChanged?.Invoke(this, new MediaChangedEventArgs
{
@ -1194,14 +1197,17 @@ namespace Emby.Dlna.PlayTo
});
}
bool _disposed;
/// <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)
@ -1220,9 +1226,10 @@ namespace Emby.Dlna.PlayTo
_disposed = true;
}
/// <inheritdoc />
public override string ToString()
{
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
}
}
}

@ -8,6 +8,9 @@ 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";
@ -33,7 +36,6 @@ namespace Emby.Dlna.PlayTo
public string PresentationUrl { get; set; }
private string _baseUrl = string.Empty;
public string BaseUrl
{
get => _baseUrl;
@ -42,7 +44,6 @@ namespace Emby.Dlna.PlayTo
public DeviceIcon Icon { get; set; }
private readonly List<DeviceService> _services = new List<DeviceService>();
public List<DeviceService> Services => _services;
public DeviceIdentification ToDeviceIdentification()

@ -0,0 +1,13 @@
#pragma warning disable CS1591
using System;
namespace Emby.Dlna.PlayTo
{
public class MediaChangedEventArgs : EventArgs
{
public UBaseObject OldMediaInfo { get; set; }
public UBaseObject NewMediaInfo { get; set; }
}
}

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.Didl;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@ -18,7 +19,6 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities;
@ -31,7 +31,6 @@ namespace Emby.Dlna.PlayTo
{
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private Device _device;
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
@ -50,6 +49,7 @@ namespace Emby.Dlna.PlayTo
private readonly string _accessToken;
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
private Device _device;
private int _currentPlaylistIndex;
private bool _disposed;
@ -372,8 +372,13 @@ namespace Emby.Dlna.PlayTo
if (!command.ControllingUserId.Equals(Guid.Empty))
{
_sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
_session.DeviceName, _session.RemoteEndPoint, user);
_sessionManager.LogSessionActivity(
_session.Client,
_session.ApplicationVersion,
_session.DeviceId,
_session.DeviceName,
_session.RemoteEndPoint,
user);
}
return PlayItems(playlist, cancellationToken);
@ -498,42 +503,44 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio)
{
return new ContentFeatureBuilder(profile)
.BuildAudioHeader(streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
streamInfo.TargetAudioSampleRate,
streamInfo.TargetAudioChannels,
streamInfo.TargetAudioBitDepth,
streamInfo.IsDirectStream,
streamInfo.RunTimeTicks ?? 0,
streamInfo.TranscodeSeekInfo);
.BuildAudioHeader(
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 = new ContentFeatureBuilder(profile)
.BuildVideoHeader(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.TargetVideoLevel,
streamInfo.TargetFramerate ?? 0,
streamInfo.TargetPacketLength,
streamInfo.TranscodeSeekInfo,
streamInfo.IsTargetAnamorphic,
streamInfo.IsTargetInterlaced,
streamInfo.TargetRefFrames,
streamInfo.TargetVideoStreamCount,
streamInfo.TargetAudioStreamCount,
streamInfo.TargetVideoCodecTag,
streamInfo.IsTargetAVC);
.BuildVideoHeader(
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.TargetVideoLevel,
streamInfo.TargetFramerate ?? 0,
streamInfo.TargetPacketLength,
streamInfo.TranscodeSeekInfo,
streamInfo.IsTargetAnamorphic,
streamInfo.IsTargetInterlaced,
streamInfo.TargetRefFrames,
streamInfo.TargetVideoStreamCount,
streamInfo.TargetAudioStreamCount,
streamInfo.TargetVideoCodecTag,
streamInfo.IsTargetAVC);
return list.Count == 0 ? null : list[0];
}
@ -633,6 +640,10 @@ namespace Emby.Dlna.PlayTo
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)
@ -673,48 +684,41 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
if (command.Arguments.TryGetValue("Index", out string arg))
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
{
return SetAudioStreamIndex(val);
}
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
return SetAudioStreamIndex(val);
}
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
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 (command.Arguments.TryGetValue("Index", out string arg))
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
return SetSubtitleStreamIndex(val);
}
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
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 (command.Arguments.TryGetValue("Volume", out string arg))
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
{
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
throw new ArgumentException("Unsupported volume value supplied.");
return _device.SetVolume(volume, cancellationToken);
}
throw new ArgumentException("Volume argument cannot be null");
throw new ArgumentException("Unsupported volume value supplied.");
}
throw new ArgumentException("Volume argument cannot be null");
default:
return Task.CompletedTask;
}
@ -778,7 +782,7 @@ namespace Emby.Dlna.PlayTo
const int maxWait = 15000000;
const int interval = 500;
var currentWait = 0;
while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
while (_device.TransportState != TransportState.Playing && currentWait < maxWait)
{
await Task.Delay(interval).ConfigureAwait(false);
currentWait += interval;
@ -787,8 +791,67 @@ namespace Emby.Dlna.PlayTo
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>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
if (_device == null)
{
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
}
private class StreamParams
{
private MediaSourceInfo mediaSource;
private IMediaSourceManager _mediaSourceManager;
public Guid ItemId { get; set; }
public bool IsDirectStream { get; set; }
@ -809,15 +872,11 @@ namespace Emby.Dlna.PlayTo
public BaseItem Item { get; set; }
private MediaSourceInfo MediaSource;
private IMediaSourceManager _mediaSourceManager;
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
{
if (MediaSource != null)
if (mediaSource != null)
{
return MediaSource;
return mediaSource;
}
var hasMediaSources = Item as IHasMediaSources;
@ -827,9 +886,9 @@ namespace Emby.Dlna.PlayTo
return null;
}
MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
return MediaSource;
return mediaSource;
}
private static Guid GetItemId(string url)
@ -901,61 +960,5 @@ namespace Emby.Dlna.PlayTo
return request;
}
}
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>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
if (_device == null)
{
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
}
}
}

@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@ -16,7 +17,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@ -92,7 +92,7 @@ namespace Emby.Dlna.PlayTo
// It has to report that it's a media renderer
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
{
// _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
return;
@ -174,7 +174,7 @@ namespace Emby.Dlna.PlayTo
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false);
string deviceName = device.Properties.Name;
@ -192,20 +192,20 @@ namespace Emby.Dlna.PlayTo
controller = new PlayToController(
sessionInfo,
_sessionManager,
_libraryManager,
_logger,
_dlnaManager,
_userManager,
_imageProcessor,
serverAddress,
null,
_deviceDiscovery,
_userDataManager,
_localization,
_mediaSourceManager,
_config,
_mediaEncoder);
_sessionManager,
_libraryManager,
_logger,
_dlnaManager,
_userManager,
_imageProcessor,
serverAddress,
null,
_deviceDiscovery,
_userDataManager,
_localization,
_mediaSourceManager,
_config,
_mediaEncoder);
sessionInfo.AddController(controller);
@ -218,17 +218,17 @@ namespace Emby.Dlna.PlayTo
{
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
SupportedCommands = new string[]
SupportedCommands = new[]
{
GeneralCommandType.VolumeDown.ToString(),
GeneralCommandType.VolumeUp.ToString(),
GeneralCommandType.Mute.ToString(),
GeneralCommandType.Unmute.ToString(),
GeneralCommandType.ToggleMute.ToString(),
GeneralCommandType.SetVolume.ToString(),
GeneralCommandType.SetAudioStreamIndex.ToString(),
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
GeneralCommandType.PlayMediaSource.ToString()
GeneralCommandType.VolumeDown.ToString(),
GeneralCommandType.VolumeUp.ToString(),
GeneralCommandType.Mute.ToString(),
GeneralCommandType.Unmute.ToString(),
GeneralCommandType.ToggleMute.ToString(),
GeneralCommandType.SetVolume.ToString(),
GeneralCommandType.SetAudioStreamIndex.ToString(),
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
GeneralCommandType.PlayMediaSource.ToString()
},
SupportsMediaControl = true
@ -247,8 +247,9 @@ namespace Emby.Dlna.PlayTo
{
_disposeCancellationTokenSource.Cancel();
}
catch
catch (Exception ex)
{
_logger.LogDebug(ex, "Error while disposing PlayToManager");
}
_sessionLock.Dispose();

@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackProgressEventArgs : EventArgs
{
public uBaseObject MediaInfo { get; set; }
public UBaseObject MediaInfo { get; set; }
}
}

@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStartEventArgs : EventArgs
{
public uBaseObject MediaInfo { get; set; }
public UBaseObject MediaInfo { get; set; }
}
}

@ -6,13 +6,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStoppedEventArgs : EventArgs
{
public uBaseObject MediaInfo { get; set; }
}
public class MediaChangedEventArgs : EventArgs
{
public uBaseObject OldMediaInfo { get; set; }
public uBaseObject NewMediaInfo { get; set; }
public UBaseObject MediaInfo { get; set; }
}
}

@ -1,13 +0,0 @@
#pragma warning disable CS1591
namespace Emby.Dlna.PlayTo
{
public enum TRANSPORTSTATE
{
STOPPED,
PLAYING,
TRANSITIONING,
PAUSED_PLAYBACK,
PAUSED
}
}

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Emby.Dlna.Common;
@ -11,36 +12,30 @@ 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>";
private List<StateVariable> _stateVariables = new List<StateVariable>();
public List<StateVariable> StateVariables
{
get => _stateVariables;
set => _stateVariables = value;
}
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<ServiceAction> ServiceActions
{
get => _serviceActions;
set => _serviceActions = value;
}
public List<StateVariable> StateVariables => _stateVariables;
public List<ServiceAction> ServiceActions => _serviceActions;
public static TransportCommands Create(XDocument document)
{
var command = new TransportCommands();
var actionList = document.Descendants(uPnpNamespaces.svc + "actionList");
var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
{
command.ServiceActions.Add(ServiceActionFromXml(container));
}
var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
if (stateValues != null)
{
foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
{
command.StateVariables.Add(FromXml(container));
}
@ -51,19 +46,19 @@ namespace Emby.Dlna.PlayTo
private static ServiceAction ServiceActionFromXml(XElement container)
{
var argumentList = new List<Argument>();
var serviceAction = new ServiceAction
{
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
};
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
var argumentList = serviceAction.ArgumentList;
foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
{
argumentList.Add(ArgumentFromXml(arg));
}
return new ServiceAction
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
ArgumentList = argumentList
};
return serviceAction;
}
private static Argument ArgumentFromXml(XElement container)
@ -75,29 +70,29 @@ namespace Emby.Dlna.PlayTo
return new Argument
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
};
}
private static StateVariable FromXml(XElement container)
{
var allowedValues = new List<string>();
var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
.FirstOrDefault();
if (element != null)
{
var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
allowedValues.AddRange(values.Select(child => child.Value));
}
return new StateVariable
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
AllowedValues = allowedValues.ToArray()
};
}
@ -123,7 +118,7 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
@ -147,7 +142,7 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
@ -170,7 +165,7 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
@ -180,15 +175,12 @@ namespace Emby.Dlna.PlayTo
if (state != null)
{
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
state.AllowedValues.FirstOrDefault() ??
value;
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
}
return string.Format("<{0}>{1}</{0}>", argument.Name, value);
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
}
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>";
}
}

@ -0,0 +1,14 @@
#pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.PlayTo
{
public enum TransportState
{
Stopped,
Playing,
Transitioning,
PausedPlayback,
Paused
}
}

@ -6,22 +6,22 @@ using Emby.Dlna.Ssdp;
namespace Emby.Dlna.PlayTo
{
public class UpnpContainer : uBaseObject
public class UpnpContainer : UBaseObject
{
public static uBaseObject Create(XElement container)
public static UBaseObject Create(XElement container)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
return new uBaseObject
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.uClass)
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)
};
}
}

@ -1,10 +1,11 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace Emby.Dlna.PlayTo
{
public class uBaseObject
public class UBaseObject
{
public string Id { get; set; }
@ -20,20 +21,10 @@ namespace Emby.Dlna.PlayTo
public string Url { get; set; }
public string[] ProtocolInfo { get; set; }
public IReadOnlyList<string> ProtocolInfo { get; set; }
public string UpnpClass { get; set; }
public bool Equals(uBaseObject obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
return string.Equals(Id, obj.Id);
}
public string MediaType
{
get
@ -58,5 +49,15 @@ namespace Emby.Dlna.PlayTo
return null;
}
}
public bool Equals(UBaseObject obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
return string.Equals(Id, obj.Id, StringComparison.Ordinal);
}
}
}

@ -4,38 +4,64 @@ using System.Xml.Linq;
namespace Emby.Dlna.PlayTo
{
public class uPnpNamespaces
public static class UPnpNamespaces
{
public static XNamespace dc = "http://purl.org/dc/elements/1.1/";
public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
public static XNamespace svc = "urn:schemas-upnp-org:service-1-0";
public static XNamespace ud = "urn:schemas-upnp-org:device-1-0";
public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1";
public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1";
public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1";
public static XName containers = ns + "container";
public static XName items = ns + "item";
public static XName title = dc + "title";
public static XName creator = dc + "creator";
public static XName artist = upnp + "artist";
public static XName Id = "id";
public static XName ParentId = "parentID";
public static XName uClass = upnp + "class";
public static XName Artwork = upnp + "albumArtURI";
public static XName Description = dc + "description";
public static XName LongDescription = upnp + "longDescription";
public static XName Album = upnp + "album";
public static XName Author = upnp + "author";
public static XName Director = upnp + "director";
public static XName PlayCount = upnp + "playbackCount";
public static XName Tracknumber = upnp + "originalTrackNumber";
public static XName Res = ns + "res";
public static XName Duration = "duration";
public static XName ProtocolInfo = "protocolInfo";
public static XName ServiceStateTable = svc + "serviceStateTable";
public static XName StateVariable = svc + "stateVariable";
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";
}
}

@ -64,14 +64,14 @@ namespace Emby.Dlna.Profiles
new DirectPlayProfile
{
// play all
Container = "",
Container = string.Empty,
Type = DlnaProfileType.Video
},
new DirectPlayProfile
{
// play all
Container = "",
Container = string.Empty,
Type = DlnaProfileType.Audio
}
};

@ -24,7 +24,7 @@ namespace Emby.Dlna.Profiles
{
Match = HeaderMatchType.Substring,
Name = "User-Agent",
Value ="Zip_"
Value = "Zip_"
}
}
};
@ -81,7 +81,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -124,7 +124,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile
{
Type = CodecType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -161,7 +161,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "ac3,he-aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -177,7 +177,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -192,7 +192,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile
{
Type = CodecType.VideoAudio,
Conditions = new []
Conditions = new[]
{
// The device does not have any audio switching capabilities
new ProfileCondition

@ -84,7 +84,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
}
};
ResponseProfiles = new ResponseProfile[]
ResponseProfiles = new[]
{
new ResponseProfile
{

@ -32,7 +32,7 @@ namespace Emby.Dlna.Profiles
}
};
ResponseProfiles = new ResponseProfile[]
ResponseProfiles = new[]
{
new ResponseProfile
{

@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -93,8 +93,8 @@ namespace Emby.Dlna.Profiles
new CodecProfile
{
Type = CodecType.Video,
Codec="h264",
Conditions = new []
Codec = "h264",
Conditions = new[]
{
new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"),
new ProfileCondition
@ -122,7 +122,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile
{
Type = CodecType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Audio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -182,7 +182,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Audio,
Codec = "mp3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -202,7 +202,7 @@ namespace Emby.Dlna.Profiles
}
};
ResponseProfiles = new ResponseProfile[]
ResponseProfiles = new[]
{
new ResponseProfile
{

@ -139,7 +139,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -114,7 +114,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -156,7 +156,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -172,7 +172,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
VideoCodec = "h264,mpeg4,vc1",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video
},

@ -102,13 +102,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -128,13 +128,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -148,28 +148,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="mpeg2video",
VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video",
VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video
}
};
@ -180,7 +180,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -204,7 +204,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -243,7 +243,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "mpeg2video",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -275,7 +275,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile
{
Type = CodecType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -303,7 +303,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -319,7 +319,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -341,7 +341,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "mp3,mp2",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -120,7 +120,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -143,13 +143,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -169,13 +169,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -189,28 +189,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="mpeg2video",
VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video",
VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video
},
new ResponseProfile
@ -227,7 +227,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -266,7 +266,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "mpeg2video",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -298,7 +298,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile
{
Type = CodecType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -326,7 +326,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -364,7 +364,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "mp3,mp2",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -131,13 +131,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -157,13 +157,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -177,28 +177,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="mpeg2video",
VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video",
VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video
},
new ResponseProfile
@ -215,7 +215,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -282,7 +282,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "mp3,mp2",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="mpeg2video",
VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video",
VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video
},
new ResponseProfile
@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
}
};
CodecProfiles = new[]
{
new CodecProfile
{
Type = CodecType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "mp3,mp2",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="h264",
AudioCodec="ac3,aac,mp3",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "ts,mpegts",
VideoCodec="mpeg2video",
VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video
},
new ResponseProfile
{
Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video",
VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video
},
new ResponseProfile
@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
}
};
CodecProfiles = new[]
{
new CodecProfile
{
Type = CodecType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "mp3,mp2",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -108,7 +108,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -133,7 +133,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -176,7 +176,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -201,7 +201,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "wmapro",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -235,7 +235,7 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "mp4,mov",
AudioCodec="aac",
AudioCodec = "aac",
MimeType = "video/mp4",
Type = DlnaProfileType.Video
},
@ -244,7 +244,7 @@ namespace Emby.Dlna.Profiles
{
Container = "avi",
MimeType = "video/divx",
OrgPn="AVI",
OrgPn = "AVI",
Type = DlnaProfileType.Video
},

@ -110,7 +110,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -135,7 +135,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "ac3",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -203,7 +203,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "wmapro",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -219,7 +219,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -237,7 +237,7 @@ namespace Emby.Dlna.Profiles
new ResponseProfile
{
Container = "mp4,mov",
AudioCodec="aac",
AudioCodec = "aac",
MimeType = "video/mp4",
Type = DlnaProfileType.Video
},
@ -246,7 +246,7 @@ namespace Emby.Dlna.Profiles
{
Container = "avi",
MimeType = "video/divx",
OrgPn="AVI",
OrgPn = "AVI",
Type = DlnaProfileType.Video
},

@ -20,7 +20,7 @@ namespace Emby.Dlna.Profiles
Headers = new[]
{
new HttpHeaderInfo {Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring},
new HttpHeaderInfo { Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring },
new HttpHeaderInfo
{
Name = "User-Agent",
@ -168,7 +168,7 @@ namespace Emby.Dlna.Profiles
{
Type = DlnaProfileType.Photo,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -193,7 +193,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -221,7 +221,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -119,7 +119,7 @@ namespace Emby.Dlna.Profiles
Type = DlnaProfileType.Video,
Container = "mp4,mov",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "mpeg4",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -187,7 +187,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "h264",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -236,7 +236,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.Video,
Codec = "wmv2,wmv3,vc1",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -284,7 +284,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile
{
Type = CodecType.Video,
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -307,7 +307,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "ac3,wmav2,wmapro",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{
@ -323,7 +323,7 @@ namespace Emby.Dlna.Profiles
{
Type = CodecType.VideoAudio,
Codec = "aac",
Conditions = new []
Conditions = new[]
{
new ProfileCondition
{

@ -15,11 +15,7 @@ namespace Emby.Dlna.Service
{
public abstract class BaseControlHandler
{
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
protected IServerConfigurationManager Config { get; }
protected ILogger Logger { get; }
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
{
@ -27,6 +23,10 @@ namespace Emby.Dlna.Service
Logger = logger;
}
protected IServerConfigurationManager Config { get; }
protected ILogger Logger { get; }
public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
try
@ -80,10 +80,10 @@ namespace Emby.Dlna.Service
{
writer.WriteStartDocument(true);
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
@ -210,15 +210,6 @@ namespace Emby.Dlna.Service
}
}
private class ControlRequestInfo
{
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request)
@ -240,5 +231,14 @@ namespace Emby.Dlna.Service
Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
}
private class ControlRequestInfo
{
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
}

@ -6,20 +6,22 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service
{
public class BaseService : IEventManager
public class BaseService : IDlnaEventManager
{
protected IEventManager EventManager;
protected IHttpClient HttpClient;
protected ILogger Logger;
protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
{
Logger = logger;
HttpClient = httpClient;
EventManager = new EventManager(logger, HttpClient);
EventManager = new DlnaEventManager(logger, HttpClient);
}
protected IDlnaEventManager EventManager { get; }
protected IHttpClient HttpClient { get; }
protected ILogger Logger { get; }
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
{
return EventManager.CancelEventSubscription(subscriptionId);

@ -10,7 +10,7 @@ namespace Emby.Dlna.Service
{
public static class ControlErrorHandler
{
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
public static ControlResponse GetResponse(Exception ex)
{
@ -26,11 +26,11 @@ namespace Emby.Dlna.Service
{
writer.WriteStartDocument(true);
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV);
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
writer.WriteElementString("faultcode", "500");
writer.WriteElementString("faultstring", ex.Message);

@ -87,7 +87,7 @@ namespace Emby.Dlna.Service
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
.Append("</dataType>");
if (item.AllowedValues.Length > 0)
if (item.AllowedValues.Count > 0)
{
builder.Append("<allowedValueList>");
foreach (var allowedValue in item.AllowedValues)

@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events;
using Rssdp;
using Rssdp.Infrastructure;
@ -17,9 +17,17 @@ namespace Emby.Dlna.Ssdp
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 />
@ -49,15 +57,6 @@ namespace Emby.Dlna.Ssdp
/// <inheritdoc />
public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
private SsdpDeviceLocator _deviceLocator;
private ISsdpCommunicationsServer _commsServer;
public DeviceDiscovery(IServerConfigurationManager config)
{
_config = config;
}
// Call this method from somewhere in your code to start the search.
public void Start(ISsdpCommunicationsServer communicationsServer)
{

@ -5,7 +5,7 @@ using System.Xml.Linq;
namespace Emby.Dlna.Ssdp
{
public static class Extensions
public static class SsdpExtensions
{
public static string GetValue(this XElement container, XName name)
{

@ -10,6 +10,15 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<ItemGroup>
@ -23,10 +32,15 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
<VersionPrefix>10.7.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->

@ -1,191 +0,0 @@
#pragma warning disable CS1591
#pragma warning disable SA1402
#pragma warning disable SA1649
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Notifications;
using MediaBrowser.Model.Services;
namespace Emby.Notifications.Api
{
[Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")]
public class GetNotifications : IReturn<NotificationResult>
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsRead { get; set; }
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? StartIndex { get; set; }
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? Limit { get; set; }
}
public class Notification
{
public string Id { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
public DateTime Date { get; set; }
public bool IsRead { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public NotificationLevel Level { get; set; }
}
public class NotificationResult
{
public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
public int TotalRecordCount { get; set; }
}
public class NotificationsSummary
{
public int UnreadCount { get; set; }
public NotificationLevel MaxUnreadNotificationLevel { get; set; }
}
[Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")]
public class GetNotificationsSummary : IReturn<NotificationsSummary>
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UserId { get; set; } = string.Empty;
}
[Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
public class GetNotificationTypes : IReturn<List<NotificationTypeInfo>>
{
}
[Route("/Notifications/Services", "GET", Summary = "Gets notification types")]
public class GetNotificationServices : IReturn<List<NameIdPair>>
{
}
[Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")]
public class AddAdminNotification : IReturnVoid
{
[ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string Name { get; set; } = string.Empty;
[ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string Description { get; set; } = string.Empty;
[ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string? ImageUrl { get; set; }
[ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string? Url { get; set; }
[ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public NotificationLevel Level { get; set; }
}
[Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")]
public class MarkRead : IReturnVoid
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
public string Ids { get; set; } = string.Empty;
}
[Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
public class MarkUnread : IReturnVoid
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
public string Ids { get; set; } = string.Empty;
}
[Authenticated]
public class NotificationsService : IService
{
private readonly INotificationManager _notificationManager;
private readonly IUserManager _userManager;
public NotificationsService(INotificationManager notificationManager, IUserManager userManager)
{
_notificationManager = notificationManager;
_userManager = userManager;
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotificationTypes request)
{
return _notificationManager.GetNotificationTypes();
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotificationServices request)
{
return _notificationManager.GetNotificationServices().ToList();
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotificationsSummary request)
{
return new NotificationsSummary();
}
public Task Post(AddAdminNotification request)
{
// This endpoint really just exists as post of a real with sickbeard
var notification = new NotificationRequest
{
Date = DateTime.UtcNow,
Description = request.Description,
Level = request.Level,
Name = request.Name,
Url = request.Url,
UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
.ToArray()
};
return _notificationManager.SendNotification(notification, CancellationToken.None);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public void Post(MarkRead request)
{
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public void Post(MarkUnread request)
{
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotifications request)
{
return new NotificationResult();
}
}
}

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
@ -13,7 +14,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Notifications;
using Microsoft.Extensions.Logging;

@ -1,590 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Notifications;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Activity
{
/// <summary>
/// Entry point for the activity logger.
/// </summary>
public sealed class ActivityLogEntryPoint : IServerEntryPoint
{
private readonly ILogger<ActivityLogEntryPoint> _logger;
private readonly IInstallationManager _installationManager;
private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager;
private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization;
private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="activityManager">The activity manager.</param>
/// <param name="localization">The localization manager.</param>
/// <param name="installationManager">The installation manager.</param>
/// <param name="subManager">The subtitle manager.</param>
/// <param name="userManager">The user manager.</param>
public ActivityLogEntryPoint(
ILogger<ActivityLogEntryPoint> logger,
ISessionManager sessionManager,
ITaskManager taskManager,
IActivityManager activityManager,
ILocalizationManager localization,
IInstallationManager installationManager,
ISubtitleManager subManager,
IUserManager userManager)
{
_logger = logger;
_sessionManager = sessionManager;
_taskManager = taskManager;
_activityManager = activityManager;
_localization = localization;
_installationManager = installationManager;
_subManager = subManager;
_userManager = userManager;
}
/// <inheritdoc />
public Task RunAsync()
{
_taskManager.TaskCompleted += OnTaskCompleted;
_installationManager.PluginInstalled += OnPluginInstalled;
_installationManager.PluginUninstalled += OnPluginUninstalled;
_installationManager.PluginUpdated += OnPluginUpdated;
_installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
_sessionManager.SessionStarted += OnSessionStarted;
_sessionManager.AuthenticationFailed += OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
_sessionManager.SessionEnded += OnSessionEnded;
_sessionManager.PlaybackStart += OnPlaybackStart;
_sessionManager.PlaybackStopped += OnPlaybackStopped;
_subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
_userManager.OnUserCreated += OnUserCreated;
_userManager.OnUserPasswordChanged += OnUserPasswordChanged;
_userManager.OnUserDeleted += OnUserDeleted;
_userManager.OnUserLockedOut += OnUserLockedOut;
return Task.CompletedTask;
}
private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserLockedOutWithName"),
e.Argument.Username),
NotificationType.UserLockedOut.ToString(),
e.Argument.Id)
{
LogSeverity = LogLevel.Error
}).ConfigureAwait(false);
}
private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
e.Provider,
Notifications.NotificationEntryPoint.GetItemName(e.Item)),
"SubtitleDownloadFailure",
Guid.Empty)
{
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
ShortOverview = e.Exception.Message
}).ConfigureAwait(false);
}
private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
var item = e.MediaInfo;
if (item == null)
{
_logger.LogWarning("PlaybackStopped reported with null media info.");
return;
}
if (e.Item != null && e.Item.IsThemeMedia)
{
// Don't report theme song or local trailer playback
return;
}
if (e.Users.Count == 0)
{
return;
}
var user = e.Users[0];
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackStoppedNotificationType(item.MediaType),
user.Id))
.ConfigureAwait(false);
}
private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
var item = e.MediaInfo;
if (item == null)
{
_logger.LogWarning("PlaybackStart reported with null media info.");
return;
}
if (e.Item != null && e.Item.IsThemeMedia)
{
// Don't report theme song or local trailer playback
return;
}
if (e.Users.Count == 0)
{
return;
}
var user = e.Users.First();
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackNotificationType(item.MediaType),
user.Id))
.ConfigureAwait(false);
}
private static string GetItemName(BaseItemDto item)
{
var name = item.Name;
if (!string.IsNullOrEmpty(item.SeriesName))
{
name = item.SeriesName + " - " + name;
}
if (item.Artists != null && item.Artists.Count > 0)
{
name = item.Artists[0] + " - " + name;
}
return name;
}
private static string GetPlaybackNotificationType(string mediaType)
{
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.AudioPlayback.ToString();
}
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.VideoPlayback.ToString();
}
return null;
}
private static string GetPlaybackStoppedNotificationType(string mediaType)
{
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.AudioPlaybackStopped.ToString();
}
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.VideoPlaybackStopped.ToString();
}
return null;
}
private async void OnSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (string.IsNullOrEmpty(session.UserName))
{
return;
}
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOfflineFromDevice"),
session.UserName,
session.DeviceName),
"SessionEnded",
session.UserId)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint),
}).ConfigureAwait(false);
}
private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
{
var user = e.Argument.User;
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
user.Name),
"AuthenticationSucceeded",
user.Id)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.SessionInfo.RemoteEndPoint),
}).ConfigureAwait(false);
}
private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
e.Argument.Username),
"AuthenticationFailed",
Guid.Empty)
{
LogSeverity = LogLevel.Error,
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.RemoteEndPoint),
}).ConfigureAwait(false);
}
private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserDeletedWithName"),
e.Argument.Username),
"UserDeleted",
Guid.Empty))
.ConfigureAwait(false);
}
private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPasswordChangedWithName"),
e.Argument.Username),
"UserPasswordChanged",
e.Argument.Id))
.ConfigureAwait(false);
}
private async void OnUserCreated(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserCreatedWithName"),
e.Argument.Username),
"UserCreated",
e.Argument.Id))
.ConfigureAwait(false);
}
private async void OnSessionStarted(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (string.IsNullOrEmpty(session.UserName))
{
return;
}
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOnlineFromDevice"),
session.UserName,
session.DeviceName),
"SessionStarted",
session.UserId)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint)
}).ConfigureAwait(false);
}
private async void OnPluginUpdated(object sender, InstallationInfo e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUpdatedWithName"),
e.Name),
NotificationType.PluginUpdateInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
e.Version),
Overview = e.Changelog
}).ConfigureAwait(false);
}
private async void OnPluginUninstalled(object sender, IPlugin e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUninstalledWithName"),
e.Name),
NotificationType.PluginUninstalled.ToString(),
Guid.Empty))
.ConfigureAwait(false);
}
private async void OnPluginInstalled(object sender, InstallationInfo e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginInstalledWithName"),
e.Name),
NotificationType.PluginInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
e.Version)
}).ConfigureAwait(false);
}
private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
{
var installationInfo = e.InstallationInfo;
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameInstallFailed"),
installationInfo.Name),
NotificationType.InstallationFailed.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
installationInfo.Version),
Overview = e.Exception.Message
}).ConfigureAwait(false);
}
private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
{
var result = e.Result;
var task = e.Task;
if (task.ScheduledTask is IConfigurableScheduledTask activityTask
&& !activityTask.IsLogged)
{
return;
}
var time = result.EndTimeUtc - result.StartTimeUtc;
var runningTime = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelRunningTimeValue"),
ToUserFriendlyString(time));
if (result.Status == TaskCompletionStatus.Failed)
{
var vals = new List<string>();
if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
{
vals.Add(e.Result.ErrorMessage);
}
if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
{
vals.Add(e.Result.LongErrorMessage);
}
await CreateLogEntry(new ActivityLog(
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
NotificationType.TaskFailed.ToString(),
Guid.Empty)
{
LogSeverity = LogLevel.Error,
Overview = string.Join(Environment.NewLine, vals),
ShortOverview = runningTime
}).ConfigureAwait(false);
}
}
private async Task CreateLogEntry(ActivityLog entry)
=> await _activityManager.CreateAsync(entry).ConfigureAwait(false);
/// <inheritdoc />
public void Dispose()
{
_taskManager.TaskCompleted -= OnTaskCompleted;
_installationManager.PluginInstalled -= OnPluginInstalled;
_installationManager.PluginUninstalled -= OnPluginUninstalled;
_installationManager.PluginUpdated -= OnPluginUpdated;
_installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
_sessionManager.SessionStarted -= OnSessionStarted;
_sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
_sessionManager.SessionEnded -= OnSessionEnded;
_sessionManager.PlaybackStart -= OnPlaybackStart;
_sessionManager.PlaybackStopped -= OnPlaybackStopped;
_subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
_userManager.OnUserCreated -= OnUserCreated;
_userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
_userManager.OnUserDeleted -= OnUserDeleted;
_userManager.OnUserLockedOut -= OnUserLockedOut;
}
/// <summary>
/// Constructs a user-friendly string for this TimeSpan instance.
/// </summary>
private static string ToUserFriendlyString(TimeSpan span)
{
const int DaysInYear = 365;
const int DaysInMonth = 30;
// Get each non-zero value from TimeSpan component
var values = new List<string>();
// Number of years
int days = span.Days;
if (days >= DaysInYear)
{
int years = days / DaysInYear;
values.Add(CreateValueString(years, "year"));
days %= DaysInYear;
}
// Number of months
if (days >= DaysInMonth)
{
int months = days / DaysInMonth;
values.Add(CreateValueString(months, "month"));
days = days % DaysInMonth;
}
// Number of days
if (days >= 1)
{
values.Add(CreateValueString(days, "day"));
}
// Number of hours
if (span.Hours >= 1)
{
values.Add(CreateValueString(span.Hours, "hour"));
}
// Number of minutes
if (span.Minutes >= 1)
{
values.Add(CreateValueString(span.Minutes, "minute"));
}
// Number of seconds (include when 0 if no other components included)
if (span.Seconds >= 1 || values.Count == 0)
{
values.Add(CreateValueString(span.Seconds, "second"));
}
// Combine values into string
var builder = new StringBuilder();
for (int i = 0; i < values.Count; i++)
{
if (builder.Length > 0)
{
builder.Append(i == values.Count - 1 ? " and " : ", ");
}
builder.Append(values[i]);
}
// Return result
return builder.ToString();
}
/// <summary>
/// Constructs a string description of a time-span value.
/// </summary>
/// <param name="value">The value of this item.</param>
/// <param name="description">The name of this item (singular form).</param>
private static string CreateValueString(int value, string description)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0:#,##0} {1}",
value,
value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
}
}
}

@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading configuration file: {path}", path);
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
return Activator.CreateInstance(configurationType);
}

@ -1,3 +1,5 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -22,7 +24,7 @@ namespace Emby.Server.Implementations.AppBase
{
object configuration;
byte[] buffer = null;
byte[]? buffer = null;
// Use try/catch to avoid the extra file system lookup using File.Exists
try
@ -36,19 +38,23 @@ namespace Emby.Server.Implementations.AppBase
configuration = Activator.CreateInstance(type);
}
using var stream = new MemoryStream();
using var stream = new MemoryStream(buffer?.Length ?? 0);
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
var newBytes = stream.ToArray();
byte[] newBytes = stream.GetBuffer();
int newBytesLen = (int)stream.Length;
// If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !buffer.SequenceEqual(newBytes))
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
// Save it after load in case we got new items
File.WriteAllBytes(path, newBytes);
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
fs.Write(newBytes, 0, newBytesLen);
}
}
return configuration;

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -38,15 +37,15 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using MediaBrowser.Api;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@ -54,7 +53,6 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections;
@ -73,6 +71,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
@ -90,20 +89,19 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
{
@ -124,14 +122,18 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private IHttpServer _httpServer;
private IWebSocketManager _webSocketManager;
private IHttpClient _httpClient;
private string[] _urlPrefixes;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
{
get
@ -175,6 +177,8 @@ namespace Emby.Server.Implementations
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins;
/// <summary>
@ -240,9 +244,11 @@ namespace Emby.Server.Implementations
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
INetworkManager networkManager)
INetworkManager networkManager,
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
ServiceCollection = serviceCollection;
_networkManager = networkManager;
networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@ -273,6 +279,10 @@ namespace Emby.Server.Implementations
Password = ServerConfigurationManager.Configuration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
}
public string ExpandVirtualPath(string path)
@ -302,16 +312,16 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc />
public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version;
public Version ApplicationVersion { get; }
/// <inheritdoc />
public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3);
public string ApplicationVersionString { get; }
/// <summary>
/// Gets the current application user agent.
/// </summary>
/// <value>The application user agent.</value>
public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString;
public string ApplicationUserAgent { get; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
@ -442,8 +452,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
_httpServer.GlobalResponse = null;
CoreStartupHasCompleted = true;
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
@ -466,7 +475,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
public void Init(IServiceCollection serviceCollection)
public void Init()
{
HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@ -495,143 +504,143 @@ namespace Emby.Server.Implementations
DiscoverTypes();
RegisterServices(serviceCollection);
RegisterServices();
}
public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
=> _httpServer.RequestHandler(context);
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
/// </summary>
protected virtual void RegisterServices(IServiceCollection serviceCollection)
protected virtual void RegisterServices()
{
serviceCollection.AddSingleton(_startupOptions);
serviceCollection.AddMemoryCache();
ServiceCollection.AddSingleton(_startupOptions);
serviceCollection.AddSingleton(ConfigurationManager);
serviceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddMemoryCache();
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(_fileSystemManager);
serviceCollection.AddSingleton<TvdbClientManager>();
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>();
serviceCollection.AddSingleton(_networkManager);
ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
serviceCollection.AddSingleton<IIsoManager, IsoManager>();
ServiceCollection.AddSingleton(_networkManager);
serviceCollection.AddSingleton<ITaskManager, TaskManager>();
ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
serviceCollection.AddSingleton(_xmlSerializer);
ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
ServiceCollection.AddSingleton(_xmlSerializer);
serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
serviceCollection.AddSingleton<IZipClient, ZipClient>();
ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
ServiceCollection.AddSingleton<IZipClient, ZipClient>();
serviceCollection.AddSingleton<IServerApplicationHost>(this);
serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(ServerConfigurationManager);
ServiceCollection.AddSingleton(ServerConfigurationManager);
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<ServiceController>();
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
serviceCollection.AddSingleton<IDtoService, DtoService>();
ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
ServiceCollection.AddSingleton<IDtoService, DtoService>();
serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
serviceCollection.AddSingleton<LiveTvDtoService>();
serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
ServiceCollection.AddSingleton<LiveTvDtoService>();
ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
serviceCollection.AddSingleton<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
serviceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
serviceCollection.AddSingleton<EncodingHelper>();
ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
ServiceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>();
}
/// <summary>
@ -645,7 +654,7 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
_httpServer = Resolve<IHttpServer>();
_webSocketManager = Resolve<IWebSocketManager>();
_httpClient = Resolve<IHttpClient>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@ -747,7 +756,6 @@ namespace Emby.Server.Implementations
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
}
/// <summary>
@ -767,7 +775,8 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
_httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
_urlPrefixes = GetUrlPrefixes().ToArray();
_webSocketManager.Init(GetExports<IWebSocketListener>());
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
@ -831,6 +840,8 @@ namespace Emby.Server.Implementations
{
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
}
plugin.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
@ -931,7 +942,7 @@ namespace Emby.Server.Implementations
}
}
if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
@ -1031,12 +1042,6 @@ namespace Emby.Server.Implementations
}
}
// Include composable parts in the Api assembly
yield return typeof(ApiEntryPoint).Assembly;
// Include composable parts in the Dashboard assembly
yield return typeof(DashboardService).Assembly;
// Include composable parts in the Model assembly
yield return typeof(SystemInfo).Assembly;
@ -1391,6 +1396,20 @@ namespace Emby.Server.Implementations
_plugins = list.ToArray();
}
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
.Where(i => typeof(ControllerBase).IsAssignableFrom(i))
.Select(i => i.Assembly)
.Distinct();
foreach (var assembly in assemblies)
{
Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName);
yield return assembly;
}
}
public virtual void LaunchUrl(string url)
{
if (!CanLaunchWebBrowser)

@ -1,5 +1,7 @@
using System;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Browser
@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
/// <param name="appHost">The app host.</param>
public static void OpenSwaggerPage(IServerApplicationHost appHost)
{
TryOpenUrl(appHost, "/swagger/index.html");
TryOpenUrl(appHost, "/api-docs/swagger");
}
/// <summary>

@ -426,7 +426,7 @@ namespace Emby.Server.Implementations.Channels
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
.ConfigureAwait(false);
var list = mediaInfo.ToList();
_memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5));
_memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
return list;
}
@ -746,12 +746,21 @@ namespace Emby.Server.Implementations.Channels
// null if came from cache
if (itemsResult != null)
{
var internalItems = itemsResult.Items
.Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken))
.ToArray();
var items = itemsResult.Items;
var itemsLen = items.Count;
var internalItems = new Guid[itemsLen];
for (int i = 0; i < itemsLen; i++)
{
internalItems[i] = (await GetChannelItemEntityAsync(
items[i],
channelProvider,
channel.Id,
parentItem,
cancellationToken).ConfigureAwait(false)).Id;
}
var existingIds = _libraryManager.GetItemIds(query);
var deadIds = existingIds.Except(internalItems.Select(i => i.Id))
var deadIds = existingIds.Except(internalItems)
.ToArray();
foreach (var deadId in deadIds)
@ -881,7 +890,7 @@ namespace Emby.Server.Implementations.Channels
}
catch (Exception ex)
{
_logger.LogError(ex, "Error writing to channel cache file: {path}", path);
_logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
}
}
@ -963,7 +972,7 @@ namespace Emby.Server.Implementations.Channels
return item;
}
private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
{
var parentFolderId = parentFolder.Id;
@ -1165,7 +1174,7 @@ namespace Emby.Server.Implementations.Channels
}
else if (forceUpdate)
{
item.UpdateToRepository(ItemUpdateType.None, cancellationToken);
await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
}
if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)

@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public BoxSet CreateCollection(CollectionCreationOptions options)
public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options)
{
var name = options.Name;
@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Collections
// This could cause it to get re-resolved as a plain folder
var folderName = _fileSystem.GetValidFilename(name) + " [boxset]";
var parentFolder = GetCollectionsFolder(true).GetAwaiter().GetResult();
var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
if (parentFolder == null)
{
@ -169,12 +169,16 @@ namespace Emby.Server.Implementations.Collections
if (options.ItemIdList.Length > 0)
{
AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
// The initial adding of items is going to create a local metadata file
// This will cause internet metadata to be skipped as a result
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
});
await AddToCollectionAsync(
collection.Id,
options.ItemIdList.Select(x => new Guid(x)),
false,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
// The initial adding of items is going to create a local metadata file
// This will cause internet metadata to be skipped as a result
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
}).ConfigureAwait(false);
}
else
{
@ -197,18 +201,10 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public void AddToCollection(Guid collectionId, IEnumerable<string> ids)
{
AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
}
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
/// <inheritdoc />
public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
{
AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
}
private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
if (collection == null)
@ -224,15 +220,14 @@ namespace Emby.Server.Implementations.Collections
foreach (var id in ids)
{
var guidId = new Guid(id);
var item = _libraryManager.GetItemById(guidId);
var item = _libraryManager.GetItemById(id);
if (item == null)
{
throw new ArgumentException("No item exists with the supplied Id");
}
if (!currentLinkedChildrenIds.Contains(guidId))
if (!currentLinkedChildrenIds.Contains(id))
{
itemList.Add(item);
@ -249,7 +244,7 @@ namespace Emby.Server.Implementations.Collections
collection.UpdateRatingToItems(linkedChildrenList);
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
refreshOptions.ForceSave = true;
_providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High);
@ -266,13 +261,7 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds)
{
RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i)));
}
/// <inheritdoc />
public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds)
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
@ -309,7 +298,7 @@ namespace Emby.Server.Implementations.Collections
collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray();
}
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
_providerManager.QueueRefresh(
collection.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))

@ -2,11 +2,11 @@ using System;
using System.Globalization;
using System.IO;
using Emby.Server.Implementations.AppBase;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;

@ -1,6 +1,5 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Updates;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
@ -16,10 +15,11 @@ namespace Emby.Server.Implementations
public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
{
{ HostWebClientKey, bool.TrueString },
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
{ DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString }
{ PlaylistsAllowDuplicatesKey, bool.TrueString },
{ BindToUnixSocketKey, bool.FalseString }
};
}
}

@ -978,7 +978,10 @@ namespace Emby.Server.Implementations.Data
continue;
}
str.Append($"{i.Key}={i.Value}|");
str.Append(i.Key)
.Append('=')
.Append(i.Value)
.Append('|');
}
if (str.Length == 0)
@ -1032,8 +1035,8 @@ namespace Emby.Server.Implementations.Data
continue;
}
str.Append(ToValueString(i))
.Append('|');
AppendItemImageInfo(str, i);
str.Append('|');
}
str.Length -= 1; // Remove last |
@ -1067,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
item.ImageInfos = list.ToArray();
}
public string ToValueString(ItemImageInfo image)
public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
{
const string Delimeter = "*";
const char Delimiter = '*';
var path = image.Path ?? string.Empty;
var hash = image.BlurHash ?? string.Empty;
return GetPathToSave(path) +
Delimeter +
image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
Delimeter +
image.Type +
Delimeter +
image.Width.ToString(CultureInfo.InvariantCulture) +
Delimeter +
image.Height.ToString(CultureInfo.InvariantCulture) +
Delimeter +
// Replace delimiters with other characters.
// This can be removed when we migrate to a proper DB.
hash.Replace('*', '/').Replace('|', '\\');
bldr.Append(GetPathToSave(path))
.Append(Delimiter)
.Append(image.DateModified.Ticks)
.Append(Delimiter)
.Append(image.Type)
.Append(Delimiter)
.Append(image.Width)
.Append(Delimiter)
.Append(image.Height)
.Append(Delimiter)
// Replace delimiters with other characters.
// This can be removed when we migrate to a proper DB.
.Append(hash.Replace('*', '/').Replace('|', '\\'));
}
public ItemImageInfo ItemImageInfoFromValueString(string value)
@ -4305,7 +4308,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add("ProductionYear=@Years");
if (statement != null)
{
statement.TryBind("@Years", query.Years[0].ToString());
statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
}
}
else if (query.Years.Length > 1)
@ -4557,13 +4560,13 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length > 1)
{
var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
}
if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
{
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
if (statement != null)
{
statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
@ -5167,7 +5170,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
insertText.Append(',');
}
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
insertText.AppendFormat(
CultureInfo.InvariantCulture,
"(@ItemId, @AncestorId{0}, @AncestorIdText{0})",
i.ToString(CultureInfo.InvariantCulture));
}
using (var statement = PrepareStatement(db, insertText.ToString()))
@ -5659,10 +5665,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 100;
var startIndex = 0;
const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
var insertText = new StringBuilder(StartInsertText);
while (startIndex < values.Count)
{
var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
var endIndex = Math.Min(values.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
@ -5704,6 +5710,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
insertText.Length = StartInsertText.Length;
}
}
@ -5741,10 +5748,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var startIndex = 0;
var listIndex = 0;
const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
var insertText = new StringBuilder(StartInsertText);
while (startIndex < people.Count)
{
var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
var endIndex = Math.Min(people.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
{
@ -5778,6 +5785,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
insertText.Length = StartInsertText.Length;
}
}
@ -5893,10 +5901,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 10;
var startIndex = 0;
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
while (startIndex < streams.Count)
{
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
var endIndex = Math.Min(streams.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
@ -5979,6 +5986,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
}
}
@ -6230,10 +6238,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
const int InsertAtOnce = 10;
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
{
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
for (var i = startIndex; i < endIndex; i++)
@ -6279,6 +6286,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statement.Reset();
statement.MoveNext();
}
insertText.Length = _mediaAttachmentInsertPrefix.Length;
}
}

@ -7,13 +7,13 @@ using System.IO;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Devices
lock (_capabilitiesSyncLock)
{
_memoryCache.CreateEntry(deviceId).SetValue(capabilities);
_memoryCache.Set(deviceId, capabilities);
_json.SerializeToFile(capabilities, path);
}
}

@ -73,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
/// <summary>
/// Converts a BaseItem to a DTOBaseItem.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="fields">The fields.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
/// <returns>Task{DtoBaseItem}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null)
{
var options = new DtoOptions
{
Fields = fields
};
return GetBaseItemDto(item, options, user, owner);
}
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@ -216,7 +197,7 @@ namespace Emby.Server.Implementations.Dto
catch (Exception ex)
{
// Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
_logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name);
_logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
}
}
@ -443,17 +424,6 @@ namespace Emby.Server.Implementations.Dto
return folder.GetChildCount(user);
}
/// <summary>
/// Gets client-side Id of a server-side BaseItem.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public string GetDtoId(BaseItem item)
{
return item.Id.ToString("N", CultureInfo.InvariantCulture);
}
private static void SetBookProperties(BaseItemDto dto, Book item)
{
dto.SeriesName = item.SeriesName;
@ -484,6 +454,11 @@ namespace Emby.Server.Implementations.Dto
}
}
private string GetDtoId(BaseItem item)
{
return item.Id.ToString("N", CultureInfo.InvariantCulture);
}
private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
{
if (!string.IsNullOrEmpty(item.Album))
@ -513,19 +488,6 @@ namespace Emby.Server.Implementations.Dto
.ToArray();
}
private string GetImageCacheTag(BaseItem item, ImageType type)
{
try
{
return _imageProcessor.GetImageCacheTag(item, type);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting {type} image info", type);
return null;
}
}
private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
try

@ -13,10 +13,8 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
<ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
@ -24,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.5.211" />
<PackageReference Include="IPNetwork2" Version="2.5.224" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@ -34,16 +32,16 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
<PackageReference Include="Mono.Nat" Version="2.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
<PackageReference Include="sharpcompress" Version="0.25.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
<PackageReference Include="Mono.Nat" Version="2.0.2" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
<PackageReference Include="DotNet.Glob" Version="3.1.0" />
</ItemGroup>
<ItemGroup>

@ -7,11 +7,11 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events;
using Microsoft.Extensions.Logging;
using Mono.Nat;

@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -15,7 +16,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@ -23,10 +23,12 @@ namespace Emby.Server.Implementations.EntryPoints
public class LibraryChangedNotifier : IServerEntryPoint
{
/// <summary>
/// The library manager.
/// The library update duration.
/// </summary>
private readonly ILibraryManager _libraryManager;
private const int LibraryUpdateDuration = 30000;
private readonly ILibraryManager _libraryManager;
private readonly IProviderManager _providerManager;
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
private readonly ILogger<LibraryChangedNotifier> _logger;
@ -38,23 +40,10 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly List<Folder> _foldersAddedTo = new List<Folder>();
private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
/// <summary>
/// Gets or sets the library update timer.
/// </summary>
/// <value>The library update timer.</value>
private Timer LibraryUpdateTimer { get; set; }
/// <summary>
/// The library update duration.
/// </summary>
private const int LibraryUpdateDuration = 30000;
private readonly IProviderManager _providerManager;
private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
public LibraryChangedNotifier(
ILibraryManager libraryManager,
@ -70,22 +59,26 @@ namespace Emby.Server.Implementations.EntryPoints
_providerManager = providerManager;
}
/// <summary>
/// Gets or sets the library update timer.
/// </summary>
/// <value>The library update timer.</value>
private Timer LibraryUpdateTimer { get; set; }
public Task RunAsync()
{
_libraryManager.ItemAdded += libraryManager_ItemAdded;
_libraryManager.ItemUpdated += libraryManager_ItemUpdated;
_libraryManager.ItemRemoved += libraryManager_ItemRemoved;
_libraryManager.ItemAdded += OnLibraryItemAdded;
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
_libraryManager.ItemRemoved += OnLibraryItemRemoved;
_providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
_providerManager.RefreshStarted += _providerManager_RefreshStarted;
_providerManager.RefreshProgress += _providerManager_RefreshProgress;
_providerManager.RefreshCompleted += OnProviderRefreshCompleted;
_providerManager.RefreshStarted += OnProviderRefreshStarted;
_providerManager.RefreshProgress += OnProviderRefreshProgress;
return Task.CompletedTask;
}
private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
{
var item = e.Argument.Item1;
@ -122,9 +115,11 @@ namespace Emby.Server.Implementations.EntryPoints
foreach (var collectionFolder in collectionFolders)
{
var collectionFolderDict = new Dictionary<string, string>();
collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
var collectionFolderDict = new Dictionary<string, string>
{
["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
};
try
{
@ -136,21 +131,19 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
{
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
}
private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
{
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
}
private static bool EnableRefreshMessage(BaseItem item)
{
var folder = item as Folder;
if (folder == null)
if (!(item is Folder folder))
{
return false;
}
@ -183,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@ -205,8 +198,7 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
}
var parent = e.Item.GetParent() as Folder;
if (parent != null)
if (e.Item.GetParent() is Folder parent)
{
_foldersAddedTo.Add(parent);
}
@ -220,7 +212,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@ -231,8 +223,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer == null)
{
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
Timeout.Infinite);
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
else
{
@ -248,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@ -259,16 +250,14 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer == null)
{
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
Timeout.Infinite);
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
else
{
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
}
var parent = e.Parent as Folder;
if (parent != null)
if (e.Parent is Folder parent)
{
_foldersRemovedFrom.Add(parent);
}
@ -486,13 +475,13 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer = null;
}
_libraryManager.ItemAdded -= libraryManager_ItemAdded;
_libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
_libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
_libraryManager.ItemAdded -= OnLibraryItemAdded;
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
_providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
_providerManager.RefreshStarted -= _providerManager_RefreshStarted;
_providerManager.RefreshProgress -= _providerManager_RefreshProgress;
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
}
}
}

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
@ -43,22 +44,22 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
}

@ -1,210 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Updates;
namespace Emby.Server.Implementations.EntryPoints
{
/// <summary>
/// Class WebSocketEvents.
/// </summary>
public class ServerEventNotifier : IServerEntryPoint
{
/// <summary>
/// The user manager.
/// </summary>
private readonly IUserManager _userManager;
/// <summary>
/// The installation manager.
/// </summary>
private readonly IInstallationManager _installationManager;
/// <summary>
/// The kernel.
/// </summary>
private readonly IServerApplicationHost _appHost;
/// <summary>
/// The task manager.
/// </summary>
private readonly ITaskManager _taskManager;
private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="ServerEventNotifier"/> class.
/// </summary>
/// <param name="appHost">The application host.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="installationManager">The installation manager.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="sessionManager">The session manager.</param>
public ServerEventNotifier(
IServerApplicationHost appHost,
IUserManager userManager,
IInstallationManager installationManager,
ITaskManager taskManager,
ISessionManager sessionManager)
{
_userManager = userManager;
_installationManager = installationManager;
_appHost = appHost;
_taskManager = taskManager;
_sessionManager = sessionManager;
}
/// <inheritdoc />
public Task RunAsync()
{
_userManager.OnUserDeleted += OnUserDeleted;
_userManager.OnUserUpdated += OnUserUpdated;
_appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
_installationManager.PluginUninstalled += OnPluginUninstalled;
_installationManager.PackageInstalling += OnPackageInstalling;
_installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled;
_installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted;
_installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
_taskManager.TaskCompleted += OnTaskCompleted;
return Task.CompletedTask;
}
private async void OnPackageInstalling(object sender, InstallationInfo e)
{
await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false);
}
private async void OnPackageInstallationCancelled(object sender, InstallationInfo e)
{
await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false);
}
private async void OnPackageInstallationCompleted(object sender, InstallationInfo e)
{
await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false);
}
private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
{
await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false);
}
private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
{
await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false);
}
/// <summary>
/// Installations the manager_ plugin uninstalled.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The e.</param>
private async void OnPluginUninstalled(object sender, IPlugin e)
{
await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false);
}
/// <summary>
/// Handles the HasPendingRestartChanged event of the kernel control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
private async void OnHasPendingRestartChanged(object sender, EventArgs e)
{
await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// Users the manager_ user updated.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The e.</param>
private async void OnUserUpdated(object sender, GenericEventArgs<User> e)
{
var dto = _userManager.GetUserDto(e.Argument);
await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false);
}
/// <summary>
/// Users the manager_ user deleted.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The e.</param>
private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
{
await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
private async Task SendMessageToAdminSessions<T>(string name, T data)
{
try
{
await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception)
{
}
}
private async Task SendMessageToUserSession<T>(User user, string name, T data)
{
try
{
await _sessionManager.SendMessageToUserSessions(
new List<Guid> { user.Id },
name,
data,
CancellationToken.None).ConfigureAwait(false);
}
catch (Exception)
{
}
}
/// <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 (dispose)
{
_userManager.OnUserDeleted -= OnUserDeleted;
_userManager.OnUserUpdated -= OnUserUpdated;
_installationManager.PluginUninstalled -= OnPluginUninstalled;
_installationManager.PackageInstalling -= OnPackageInstalling;
_installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled;
_installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted;
_installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
_appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged;
_taskManager.TaskCompleted -= OnTaskCompleted;
}
}
}
}

@ -1,250 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
public class FileWriter : IHttpResult
{
private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private static readonly string[] _skipLogExtensions = {
".js",
".html",
".css"
};
private readonly IStreamHelper _streamHelper;
private readonly ILogger _logger;
/// <summary>
/// The _options.
/// </summary>
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
/// <summary>
/// The _requested ranges.
/// </summary>
private List<KeyValuePair<long, long?>> _requestedRanges;
public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
_streamHelper = streamHelper;
Path = path;
_logger = logger;
RangeHeader = rangeHeader;
Headers[HeaderNames.ContentType] = contentType;
TotalContentLength = fileSystem.GetFileInfo(path).Length;
Headers[HeaderNames.AcceptRanges] = "bytes";
if (string.IsNullOrWhiteSpace(rangeHeader))
{
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
StatusCode = HttpStatusCode.OK;
}
else
{
StatusCode = HttpStatusCode.PartialContent;
SetRangeValues();
}
FileShare = FileShare.Read;
Cookies = new List<Cookie>();
}
private string RangeHeader { get; set; }
private bool IsHeadRequest { get; set; }
private long RangeStart { get; set; }
private long RangeEnd { get; set; }
private long RangeLength { get; set; }
public long TotalContentLength { get; set; }
public Action OnComplete { get; set; }
public Action OnError { get; set; }
public List<Cookie> Cookies { get; private set; }
public FileShare FileShare { get; set; }
/// <summary>
/// Gets the options.
/// </summary>
/// <value>The options.</value>
public IDictionary<string, string> Headers => _options;
public string Path { get; set; }
/// <summary>
/// Gets the requested ranges.
/// </summary>
/// <value>The requested ranges.</value>
protected List<KeyValuePair<long, long?>> RequestedRanges
{
get
{
if (_requestedRanges == null)
{
_requestedRanges = new List<KeyValuePair<long, long?>>();
// Example: bytes=0-,32-63
var ranges = RangeHeader.Split('=')[1].Split(',');
foreach (var range in ranges)
{
var vals = range.Split('-');
long start = 0;
long? end = null;
if (!string.IsNullOrEmpty(vals[0]))
{
start = long.Parse(vals[0], UsCulture);
}
if (!string.IsNullOrEmpty(vals[1]))
{
end = long.Parse(vals[1], UsCulture);
}
_requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
}
}
return _requestedRanges;
}
}
public string ContentType { get; set; }
public IRequest RequestContext { get; set; }
public object Response { get; set; }
public int Status { get; set; }
public HttpStatusCode StatusCode
{
get => (HttpStatusCode)Status;
set => Status = (int)value;
}
/// <summary>
/// Sets the range values.
/// </summary>
private void SetRangeValues()
{
var requestedRange = RequestedRanges[0];
// If the requested range is "0-", we can optimize by just doing a stream copy
if (!requestedRange.Value.HasValue)
{
RangeEnd = TotalContentLength - 1;
}
else
{
RangeEnd = requestedRange.Value.Value;
}
RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart;
// Content-Length is the length of what we're serving, not the original content
var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentLength] = lengthString;
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
Headers[HeaderNames.ContentRange] = rangeString;
_logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
}
public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
{
try
{
// Headers only
if (IsHeadRequest)
{
return;
}
var path = Path;
var offset = RangeStart;
var count = RangeLength;
if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
{
var extension = System.IO.Path.GetExtension(path);
if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
_logger.LogDebug("Transmit file {0}", path);
}
offset = 0;
count = 0;
}
await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
}
finally
{
OnComplete?.Invoke();
}
}
public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
{
var fileOptions = FileOptions.SequentialScan;
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileOptions |= FileOptions.Asynchronous;
}
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
{
if (offset > 0)
{
fs.Position = offset;
}
if (count > 0)
{
await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
}
else
{
await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
}
}
}
}
}

@ -1,766 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using ServiceStack.Text.Jsv;
namespace Emby.Server.Implementations.HttpServer
{
public class HttpListenerHost : IHttpServer
{
/// <summary>
/// The key for a setting that specifies the default redirect path
/// to use for requests where the URL base prefix is invalid or missing.
/// </summary>
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
private readonly ILogger<HttpListenerHost> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
private readonly IServerApplicationHost _appHost;
private readonly IJsonSerializer _jsonSerializer;
private readonly IXmlSerializer _xmlSerializer;
private readonly Func<Type, Func<string, object>> _funcParseFn;
private readonly string _defaultRedirectPath;
private readonly string _baseUrlPrefix;
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
private readonly IHostEnvironment _hostEnvironment;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private bool _disposed = false;
public HttpListenerHost(
IServerApplicationHost applicationHost,
ILogger<HttpListenerHost> logger,
IServerConfigurationManager config,
IConfiguration configuration,
INetworkManager networkManager,
IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer,
ILocalizationManager localizationManager,
ServiceController serviceController,
IHostEnvironment hostEnvironment,
ILoggerFactory loggerFactory)
{
_appHost = applicationHost;
_logger = logger;
_config = config;
_defaultRedirectPath = configuration[DefaultRedirectKey];
_baseUrlPrefix = _config.Configuration.BaseUrl;
_networkManager = networkManager;
_jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer;
ServiceController = serviceController;
_hostEnvironment = hostEnvironment;
_loggerFactory = loggerFactory;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
Instance = this;
ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
}
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
public static HttpListenerHost Instance { get; protected set; }
public string[] UrlPrefixes { get; private set; }
public string GlobalResponse { get; set; }
public ServiceController ServiceController { get; }
public object CreateInstance(Type type)
{
return _appHost.CreateInstance(type);
}
private static string NormalizeUrlPath(string path)
{
if (path.Length > 0 && path[0] == '/')
{
// If the path begins with a leading slash, just return it as-is
return path;
}
else
{
// If the path does not begin with a leading slash, append one for consistency
return "/" + path;
}
}
/// <summary>
/// Applies the request filters. Returns whether or not the request has been handled
/// and no more processing should be done.
/// </summary>
/// <returns></returns>
public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
{
// Exec all RequestFilter attributes with Priority < 0
var attributes = GetRequestFilterAttributes(requestDto.GetType());
int count = attributes.Count;
int i = 0;
for (; i < count && attributes[i].Priority < 0; i++)
{
var attribute = attributes[i];
attribute.RequestFilter(req, res, requestDto);
}
// Exec remaining RequestFilter attributes with Priority >= 0
for (; i < count && attributes[i].Priority >= 0; i++)
{
var attribute = attributes[i];
attribute.RequestFilter(req, res, requestDto);
}
}
public Type GetServiceTypeByRequest(Type requestType)
{
_serviceOperationsMap.TryGetValue(requestType, out var serviceType);
return serviceType;
}
public void AddServiceInfo(Type serviceType, Type requestType)
{
_serviceOperationsMap[requestType] = serviceType;
}
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
{
var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
var serviceType = GetServiceTypeByRequest(requestDtoType);
if (serviceType != null)
{
attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
}
attributes.Sort((x, y) => x.Priority - y.Priority);
return attributes;
}
private static Exception GetActualException(Exception ex)
{
if (ex is AggregateException agg)
{
var inner = agg.InnerException;
if (inner != null)
{
return GetActualException(inner);
}
else
{
var inners = agg.InnerExceptions;
if (inners.Count > 0)
{
return GetActualException(inners[0]);
}
}
}
return ex;
}
private int GetStatusCode(Exception ex)
{
switch (ex)
{
case ArgumentException _: return 400;
case AuthenticationException _: return 401;
case SecurityException _: return 403;
case DirectoryNotFoundException _:
case FileNotFoundException _:
case ResourceNotFoundException _: return 404;
case MethodNotAllowedException _: return 405;
default: return 500;
}
}
private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
{
if (ignoreStackTrace)
{
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
}
else
{
_logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
}
var httpRes = httpReq.Response;
if (httpRes.HasStarted)
{
return;
}
httpRes.StatusCode = statusCode;
var errContent = _hostEnvironment.IsDevelopment()
? (NormalizeExceptionMessage(ex) ?? string.Empty)
: "Error processing request.";
httpRes.ContentType = "text/plain";
httpRes.ContentLength = errContent.Length;
await httpRes.WriteAsync(errContent).ConfigureAwait(false);
}
private string NormalizeExceptionMessage(Exception ex)
{
// Do not expose the exception message for AuthenticationException
if (ex is AuthenticationException)
{
return null;
}
// Strip any information we don't want to reveal
return ex.Message
?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
}
public static string RemoveQueryStringByKey(string url, string key)
{
var uri = new Uri(url);
// this gets all the query string key value pairs as a collection
var newQueryString = QueryHelpers.ParseQuery(uri.Query);
var originalCount = newQueryString.Count;
if (originalCount == 0)
{
return url;
}
// this removes the key if exists
newQueryString.Remove(key);
if (originalCount == newQueryString.Count)
{
return url;
}
// this gets the page path from root without QueryString
string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
return newQueryString.Count > 0
? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
: pagePathWithoutQueryString;
}
private static string GetUrlToLog(string url)
{
url = RemoveQueryStringByKey(url, "api_key");
return url;
}
private static string NormalizeConfiguredLocalAddress(string address)
{
var add = address.AsSpan().Trim('/');
int index = add.IndexOf('/');
if (index != -1)
{
add = add.Slice(index + 1);
}
return add.TrimStart('/').ToString();
}
private bool ValidateHost(string host)
{
var hosts = _config
.Configuration
.LocalNetworkAddresses
.Select(NormalizeConfiguredLocalAddress)
.ToList();
if (hosts.Count == 0)
{
return true;
}
host ??= string.Empty;
if (_networkManager.IsInPrivateAddressSpace(host))
{
hosts.Add("localhost");
hosts.Add("127.0.0.1");
return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
}
return true;
}
private bool ValidateRequest(string remoteIp, bool isLocal)
{
if (isLocal)
{
return true;
}
if (_config.Configuration.EnableRemoteAccess)
{
var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
{
if (_config.Configuration.IsRemoteIPFilterBlacklist)
{
return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
}
else
{
return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
}
}
}
else
{
if (!_networkManager.IsInLocalNetwork(remoteIp))
{
return false;
}
}
return true;
}
/// <summary>
/// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
/// </summary>
/// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
private bool ValidateSsl(string remoteIp, string urlString)
{
if (_config.Configuration.RequireHttps
&& _appHost.ListenWithHttps
&& !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
{
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
if (!_networkManager.IsInLocalNetwork(remoteIp))
{
return false;
}
}
return true;
}
/// <inheritdoc />
public Task RequestHandler(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
return WebSocketRequestHandler(context);
}
var request = context.Request;
var response = context.Response;
var localPath = context.Request.Path.ToString();
var req = new WebSocketSharpRequest(request, response, request.Path);
return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
}
/// <summary>
/// Overridable method that can be used to implement a custom handler.
/// </summary>
private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
var httpRes = httpReq.Response;
string urlToLog = GetUrlToLog(urlString);
string remoteIp = httpReq.RemoteIp;
try
{
if (_disposed)
{
httpRes.StatusCode = 503;
httpRes.ContentType = "text/plain";
await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
return;
}
if (!ValidateHost(host))
{
httpRes.StatusCode = 400;
httpRes.ContentType = "text/plain";
await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
return;
}
if (!ValidateRequest(remoteIp, httpReq.IsLocal))
{
httpRes.StatusCode = 403;
httpRes.ContentType = "text/plain";
await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
return;
}
if (!ValidateSsl(httpReq.RemoteIp, urlString))
{
RedirectToSecureUrl(httpReq, httpRes, urlString);
return;
}
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
{
httpRes.Headers.Add(key, value);
}
httpRes.ContentType = "text/plain";
await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
return;
}
if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
|| string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
|| string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
|| string.IsNullOrEmpty(localPath)
|| !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
{
// Always redirect back to the default path if the base prefix is invalid or missing
_logger.LogDebug("Normalizing a URL at {0}", localPath);
httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
return;
}
if (!string.IsNullOrEmpty(GlobalResponse))
{
// We don't want the address pings in ApplicationHost to fail
if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
{
httpRes.StatusCode = 503;
httpRes.ContentType = "text/html";
await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
return;
}
}
var handler = GetServiceHandler(httpReq);
if (handler != null)
{
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
}
else
{
throw new FileNotFoundException();
}
}
catch (Exception requestEx)
{
try
{
var requestInnerEx = GetActualException(requestEx);
var statusCode = GetStatusCode(requestInnerEx);
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
{
if (!httpRes.Headers.ContainsKey(key))
{
httpRes.Headers.Add(key, value);
}
}
bool ignoreStackTrace =
requestInnerEx is SocketException
|| requestInnerEx is IOException
|| requestInnerEx is OperationCanceledException
|| requestInnerEx is SecurityException
|| requestInnerEx is AuthenticationException
|| requestInnerEx is FileNotFoundException;
// Do not handle 500 server exceptions manually when in development mode.
// Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
// However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
// because it will log the stack trace when it handles the exception.
if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
{
throw;
}
await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
}
catch (Exception handlerException)
{
var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
_logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
if (_hostEnvironment.IsDevelopment())
{
throw aggregateEx;
}
}
}
finally
{
if (httpRes.StatusCode >= 500)
{
_logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
}
stopWatch.Stop();
var elapsed = stopWatch.Elapsed;
if (elapsed.TotalMilliseconds > 500)
{
_logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
}
}
}
private async Task WebSocketRequestHandler(HttpContext context)
{
if (_disposed)
{
return;
}
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
context.Connection.RemoteIpAddress,
context.Request.Query)
{
OnReceive = ProcessWebSocketMessageReceived
};
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
await connection.ProcessAsync().ConfigureAwait(false);
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
}
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
{
_logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
if (!context.Response.HasStarted)
{
context.Response.StatusCode = 500;
}
}
}
/// <summary>
/// Get the default CORS headers.
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
{
var origin = req.Headers["Origin"];
if (origin == StringValues.Empty)
{
origin = req.Headers["Host"];
if (origin == StringValues.Empty)
{
origin = "*";
}
}
var headers = new Dictionary<string, string>();
headers.Add("Access-Control-Allow-Origin", origin);
headers.Add("Access-Control-Allow-Credentials", "true");
headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
return headers;
}
// Entry point for HttpListener
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
{
var pathInfo = httpReq.PathInfo;
pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
if (restPath != null)
{
return new ServiceHandler(restPath, contentType);
}
_logger.LogError("Could not find handler for {PathInfo}", pathInfo);
return null;
}
private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
{
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
var builder = new UriBuilder(uri)
{
Port = _config.Configuration.PublicHttpsPort,
Scheme = "https"
};
url = builder.Uri.ToString();
}
httpRes.Redirect(url);
}
/// <summary>
/// Adds the rest handlers.
/// </summary>
/// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
/// <param name="listeners">The web socket listeners.</param>
/// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
{
_webSocketListeners = listeners.ToArray();
UrlPrefixes = urlPrefixes.ToArray();
ServiceController.Init(this, serviceTypes);
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
{
new ResponseFilter(this, _logger).FilterResponse
};
}
public RouteAttribute[] GetRouteAttributes(Type requestType)
{
var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
var clone = routes.ToList();
foreach (var route in clone)
{
routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
Summary = route.Summary
});
routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
Summary = route.Summary
});
routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
Summary = route.Summary
});
}
return routes.ToArray();
}
public Func<string, object> GetParseFn(Type propertyType)
{
return _funcParseFn(propertyType);
}
public void SerializeToJson(object o, Stream stream)
{
_jsonSerializer.SerializeToStream(o, stream);
}
public void SerializeToXml(object o, Stream stream)
{
_xmlSerializer.SerializeToStream(o, stream);
}
public Task<object> DeserializeXml(Type type, Stream stream)
{
return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
}
public Task<object> DeserializeJson(Type type, Stream stream)
{
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
}
private string NormalizeEmbyRoutePath(string path)
{
_logger.LogDebug("Normalizing /emby route");
return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
}
private string NormalizeMediaBrowserRoutePath(string path)
{
_logger.LogDebug("Normalizing /mediabrowser route");
return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
}
private string NormalizeCustomRoutePath(string path)
{
_logger.LogDebug("Normalizing custom route {0}", path);
return _baseUrlPrefix + NormalizeUrlPath(path);
}
/// <summary>
/// Processes the web socket message received.
/// </summary>
/// <param name="result">The result.</param>
private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
{
if (_disposed)
{
return Task.CompletedTask;
}
IEnumerable<Task> GetTasks()
{
foreach (var x in _webSocketListeners)
{
yield return x.ProcessMessageAsync(result);
}
}
return Task.WhenAll(GetTasks());
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save