Merge remote-tracking branch 'upstream/master' into package-install-repo

pull/3577/head
crobibero 4 years ago
commit b7022e8dc1

@ -62,7 +62,6 @@ jobs:
- task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact'
enabled: false
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
@ -74,7 +73,6 @@ jobs:
- task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact'
enabled: false
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll'
@ -85,7 +83,6 @@ jobs:
- task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool'
enabled: false
inputs:
command: custom
custom: compat

@ -64,28 +64,28 @@ jobs:
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: false
- task: PublishPipelineArtifact@0
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Naming'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming'
- task: PublishPipelineArtifact@0
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Controller'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller'
- task: PublishPipelineArtifact@0
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Model'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model'
- task: PublishPipelineArtifact@0
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Common'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
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,8 +116,9 @@ jobs:
$(JellyfinVersion)-$(BuildConfiguration)
- job: CollectArtifacts
timeoutInMinutes: 10
timeoutInMinutes: 20
displayName: 'Collect Artifacts'
continueOnError: true
dependsOn:
- BuildPackage
- BuildDocker
@ -129,20 +130,22 @@ jobs:
steps:
- task: SSH@0
displayName: 'Update Unstable Repository'
continueOnError: true
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
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: 'commands'
commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
@ -155,7 +158,7 @@ jobs:
steps:
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
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'
@ -172,7 +175,7 @@ jobs:
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory)'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
- task: PublishBuildArtifacts@1
displayName: 'Publish Nuget packages'
@ -180,10 +183,32 @@ jobs:
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 feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
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

@ -74,7 +74,6 @@ jobs:
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: 'Run ReportGenerator'
enabled: false
inputs:
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
targetdir: "$(Agent.TempDirectory)/merged/"
@ -84,10 +83,16 @@ jobs:
- task: PublishCodeCoverageResults@1
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: 'Publish Code Coverage'
enabled: false
inputs:
codeCoverageTool: "cobertura"
#summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
pathToSources: $(Build.SourcesDirectory)
failIfCoverageEmpty: true
- task: PublishPipelineArtifact@1
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
artifactName: 'OpenAPI Spec'

@ -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

@ -17,7 +17,7 @@
"type": "process",
"args": [
"test",
"${workspaceFolder}/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj"
"${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
],
"problemMatcher": "$msCompile"
}

@ -57,6 +57,7 @@
- [Larvitar](https://github.com/Larvitar)
- [LeoVerto](https://github.com/LeoVerto)
- [Liggy](https://github.com/Liggy)
- [lmaonator](https://github.com/lmaonator)
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
- [loli10K](https://github.com/loli10K)
- [lostmypillow](https://github.com/lostmypillow)
@ -78,6 +79,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)
@ -133,6 +135,7 @@
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
# Emby Contributors
@ -196,3 +199,4 @@
- [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)

@ -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: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: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:DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/debian:buster-slim

@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System.Net.Http;
using System.Threading.Tasks;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using Microsoft.Extensions.Logging;
@ -18,8 +18,8 @@ namespace Emby.Dlna.ConnectionManager
IDlnaManager dlna,
IServerConfigurationManager config,
ILogger<ConnectionManagerService> logger,
IHttpClient httpClient)
: base(logger, httpClient)
IHttpClientFactory httpClientFactory)
: base(logger, httpClientFactory)
{
_dlna = dlna;
_config = config;

@ -2,11 +2,11 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@ -41,7 +41,7 @@ namespace Emby.Dlna.ContentDirectory
IServerConfigurationManager config,
IUserManager userManager,
ILogger<ContentDirectoryService> logger,
IHttpClient httpClient,
IHttpClientFactory httpClient,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
IUserViewManager userViewManager,

@ -1363,7 +1363,7 @@ 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());
}

@ -948,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);
}
}
@ -960,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);
}
}

@ -126,32 +126,23 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
builder.Append("ModelName:").AppendLine(profile.ModelName);
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
{
if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
{
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
{
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
{
return false;
}
@ -159,7 +150,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false;
}
@ -167,7 +158,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false;
}
@ -175,7 +166,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false;
}
@ -183,7 +174,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelName))
{
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false;
}
@ -191,7 +182,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false;
}
@ -199,7 +190,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false;
}
@ -207,7 +198,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false;
}
@ -216,11 +207,11 @@ namespace Emby.Dlna
return true;
}
private bool IsRegexMatch(string input, string pattern)
private bool IsRegexOrSubstringMatch(string input, string pattern)
{
try
{
return Regex.IsMatch(input, pattern);
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
catch (ArgumentException ex)
{
@ -511,8 +502,7 @@ namespace Emby.Dlna
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetProfile(headers) ??
GetDefaultProfile();
var profile = GetDefaultProfile();
var serverId = _appHost.SystemId;

@ -80,6 +80,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
</ItemGroup>
</Project>

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
@ -20,13 +21,13 @@ namespace Emby.Dlna.Eventing
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public DlnaEventManager(ILogger logger, IHttpClient httpClient)
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
{
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
@ -167,24 +168,17 @@ namespace Emby.Dlna.Eventing
builder.Append("</e:propertyset>");
var options = new HttpRequestOptions
{
RequestContent = builder.ToString(),
RequestContentType = "text/xml",
Url = subscription.CallbackUrl,
BufferContent = false
};
options.RequestHeaders.Add("NT", subscription.NotificationType);
options.RequestHeaders.Add("NTS", "upnp:propchange");
options.RequestHeaders.Add("SID", subscription.Id);
options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
try
{
using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
{
}
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
}
catch (OperationCanceledException)
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 278 B

@ -2,6 +2,7 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
@ -36,7 +37,7 @@ namespace Emby.Dlna.Main
private readonly ILogger<DlnaEntryPoint> _logger;
private readonly IServerApplicationHost _appHost;
private readonly ISessionManager _sessionManager;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
@ -61,7 +62,7 @@ namespace Emby.Dlna.Main
ILoggerFactory loggerFactory,
IServerApplicationHost appHost,
ISessionManager sessionManager,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IUserManager userManager,
IDlnaManager dlnaManager,
@ -79,7 +80,7 @@ namespace Emby.Dlna.Main
_config = config;
_appHost = appHost;
_sessionManager = sessionManager;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
@ -101,7 +102,7 @@ namespace Emby.Dlna.Main
config,
userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
httpClient,
httpClientFactory,
localizationManager,
mediaSourceManager,
userViewManager,
@ -112,11 +113,11 @@ namespace Emby.Dlna.Main
dlnaManager,
config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClient);
httpClientFactory);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClient,
httpClientFactory,
config);
Current = this;
}
@ -364,7 +365,7 @@ namespace Emby.Dlna.Main
_appHost,
_imageProcessor,
_deviceDiscovery,
_httpClient,
_httpClientFactory,
_config,
_userDataManager,
_localization,

@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System.Net.Http;
using System.Threading.Tasks;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Logging;
@ -14,9 +14,9 @@ namespace Emby.Dlna.MediaReceiverRegistrar
public MediaReceiverRegistrarService(
ILogger<MediaReceiverRegistrarService> logger,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config)
: base(logger, httpClient)
: base(logger, httpClientFactory)
{
_config = config;
}

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
@ -21,7 +22,7 @@ namespace Emby.Dlna.PlayTo
{
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
@ -34,10 +35,10 @@ namespace Emby.Dlna.PlayTo
private int _connectFailureCount;
private bool _disposed;
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger)
{
Properties = deviceProperties;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
@ -236,7 +237,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Setting mute");
var value = mute ? 1 : 0;
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
.ConfigureAwait(false);
IsMuted = mute;
@ -271,7 +272,7 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better
Volume = value;
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
.ConfigureAwait(false);
}
@ -292,7 +293,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(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
.ConfigureAwait(false);
RestartTimer(true);
@ -326,7 +327,7 @@ namespace Emby.Dlna.PlayTo
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
.ConfigureAwait(false);
await Task.Delay(50).ConfigureAwait(false);
@ -368,7 +369,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
return new SsdpHttpClient(_httpClient).SendCommandAsync(
return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@ -397,7 +398,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
.ConfigureAwait(false);
RestartTimer(true);
@ -415,7 +416,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
.ConfigureAwait(false);
TransportState = TransportState.Paused;
@ -542,7 +543,7 @@ namespace Emby.Dlna.PlayTo
return;
}
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@ -592,7 +593,7 @@ namespace Emby.Dlna.PlayTo
return;
}
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@ -625,7 +626,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@ -667,7 +668,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@ -734,7 +735,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@ -912,7 +913,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
var httpClient = new SsdpHttpClient(_httpClient);
var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
@ -940,7 +941,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
var httpClient = new SsdpHttpClient(_httpClient);
var httpClient = new SsdpHttpClient(_httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
@ -969,9 +970,9 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url;
}
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
{
var ssdpHttpClient = new SsdpHttpClient(httpClient);
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
@ -1079,7 +1080,7 @@ namespace Emby.Dlna.PlayTo
}
}
return new Device(deviceProperties, httpClient, logger);
return new Device(deviceProperties, httpClientFactory, logger);
}
private static DeviceIcon CreateIcon(XElement element)

@ -669,62 +669,57 @@ namespace Emby.Dlna.PlayTo
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{
if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType))
{
switch (commandType)
{
case GeneralCommandType.VolumeDown:
return _device.VolumeDown(cancellationToken);
case GeneralCommandType.VolumeUp:
return _device.VolumeUp(cancellationToken);
case GeneralCommandType.Mute:
return _device.Mute(cancellationToken);
case GeneralCommandType.Unmute:
return _device.Unmute(cancellationToken);
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
switch (command.Name)
{
case GeneralCommandType.VolumeDown:
return _device.VolumeDown(cancellationToken);
case GeneralCommandType.VolumeUp:
return _device.VolumeUp(cancellationToken);
case GeneralCommandType.Mute:
return _device.Mute(cancellationToken);
case GeneralCommandType.Unmute:
return _device.Unmute(cancellationToken);
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
if (int.TryParse(index, 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");
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
}
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
}
throw new ArgumentException("Unsupported volume value supplied.");
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
throw new ArgumentException("Volume argument cannot be null");
default:
return Task.CompletedTask;
}
}
throw new ArgumentException("Unsupported volume value supplied.");
}
return Task.CompletedTask;
throw new ArgumentException("Volume argument cannot be null");
default:
return Task.CompletedTask;
}
}
private async Task SetAudioStreamIndex(int? newIndex)

@ -4,6 +4,7 @@ using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
@ -33,7 +34,7 @@ namespace Emby.Dlna.PlayTo
private readonly IDlnaManager _dlnaManager;
private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
@ -46,7 +47,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
_logger = logger;
_sessionManager = sessionManager;
@ -56,7 +57,7 @@ namespace Emby.Dlna.PlayTo
_appHost = appHost;
_imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_config = config;
_userDataManager = userDataManager;
_localization = localization;
@ -129,25 +130,21 @@ namespace Emby.Dlna.PlayTo
}
}
private string GetUuid(string usn)
private static string GetUuid(string usn)
{
var found = false;
var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
usn = usn.Substring(index);
found = true;
}
const string UuidStr = "uuid:";
const string UuidColonStr = "::";
index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
usn = usn.Substring(0, index);
return usn.Substring(index + UuidStr.Length);
}
if (found)
index = usn.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
return usn;
usn = usn.Substring(0, index + UuidColonStr.Length);
}
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
@ -174,7 +171,7 @@ namespace Emby.Dlna.PlayTo
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false);
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
string deviceName = device.Properties.Name;

@ -4,6 +4,8 @@ using System;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -20,11 +22,11 @@ namespace Emby.Dlna.PlayTo
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
public SsdpHttpClient(IHttpClient httpClient)
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
}
public async Task<XDocument> SendCommandAsync(
@ -36,20 +38,18 @@ namespace Emby.Dlna.PlayTo
CancellationToken cancellationToken = default)
{
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
using (var response = await PostSoapDataAsync(
url,
$"\"{service.ServiceType}#{command}\"",
postData,
header,
cancellationToken)
.ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
LoadOptions.PreserveWhitespace);
}
using var response = await PostSoapDataAsync(
url,
$"\"{service.ServiceType}#{command}\"",
postData,
header,
cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
LoadOptions.PreserveWhitespace);
}
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@ -76,49 +76,32 @@ namespace Emby.Dlna.PlayTo
int eventport,
int timeOut = 3600)
{
var options = new HttpRequestOptions
{
Url = url,
UserAgent = USERAGENT,
LogErrorResponseBody = true,
BufferContent = false,
};
options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
options.RequestHeaders["NT"] = "upnp:event";
options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
{
}
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
}
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
{
var options = new HttpRequestOptions
{
Url = url,
UserAgent = USERAGENT,
LogErrorResponseBody = true,
BufferContent = false,
CancellationToken = cancellationToken
};
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
LoadOptions.PreserveWhitespace);
}
using var options = new HttpRequestMessage(HttpMethod.Get, url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
LoadOptions.PreserveWhitespace);
}
private Task<HttpResponseInfo> PostSoapDataAsync(
private async Task<HttpResponseMessage> PostSoapDataAsync(
string url,
string soapAction,
string postData,
@ -130,29 +113,20 @@ namespace Emby.Dlna.PlayTo
soapAction = $"\"{soapAction}\"";
}
var options = new HttpRequestOptions
{
Url = url,
UserAgent = USERAGENT,
LogErrorResponseBody = true,
BufferContent = false,
CancellationToken = cancellationToken
};
options.RequestHeaders["SOAPAction"] = soapAction;
options.RequestHeaders["Pragma"] = "no-cache";
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
using var options = new HttpRequestMessage(HttpMethod.Post, url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
if (!string.IsNullOrEmpty(header))
{
options.RequestHeaders["contentFeatures.dlna.org"] = header;
options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
}
options.RequestContentType = "text/xml";
options.RequestContent = postData;
options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
return _httpClient.Post(options);
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
}
}
}

@ -1,25 +1,21 @@
#pragma warning disable CS1591
using System.Net.Http;
using Emby.Dlna.Eventing;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service
{
public class BaseService : IDlnaEventManager
{
protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory)
{
Logger = logger;
HttpClient = httpClient;
EventManager = new DlnaEventManager(logger, HttpClient);
EventManager = new DlnaEventManager(logger, httpClientFactory);
}
protected IDlnaEventManager EventManager { get; }
protected IHttpClient HttpClient { get; }
protected ILogger Logger { get; }
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)

@ -455,7 +455,7 @@ namespace Emby.Drawing
throw new ArgumentException("Path can't be empty.", nameof(path));
}
if (path.IsEmpty)
if (filename.IsEmpty)
{
throw new ArgumentException("Filename can't be empty.", nameof(filename));
}

@ -1,6 +1,6 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@ -19,12 +19,7 @@ namespace Emby.Naming.AudioBook
public AudioBookFilePathParserResult Parse(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
var result = new AudioBookFilePathParserResult();
AudioBookFilePathParserResult result = default;
var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions)
{
@ -50,27 +45,14 @@ namespace Emby.Naming.AudioBook
{
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.ChapterNumber = intValue;
result.PartNumber = intValue;
}
}
}
}
}
/*var matches = _iRegexProvider.GetRegex("\\d+", RegexOptions.IgnoreCase).Matches(fileName);
if (matches.Count > 0)
{
if (!result.ChapterNumber.HasValue)
{
result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
}
if (matches.Count > 1)
{
result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);
}
}*/
result.Success = result.PartNumber.HasValue || result.ChapterNumber.HasValue;
result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
return result;
}

@ -1,8 +1,9 @@
#nullable enable
#pragma warning disable CS1591
namespace Emby.Naming.AudioBook
{
public class AudioBookFilePathParserResult
public struct AudioBookFilePathParserResult
{
public int? PartNumber { get; set; }

@ -55,8 +55,8 @@ namespace Emby.Naming.AudioBook
{
Path = path,
Container = container,
PartNumber = parsingResult.PartNumber,
ChapterNumber = parsingResult.ChapterNumber,
PartNumber = parsingResult.PartNumber,
IsDirectory = isDirectory
};
}

@ -136,8 +136,8 @@ namespace Emby.Naming.Common
CleanDateTimes = new[]
{
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
};
CleanStrings = new[]

@ -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>
@ -28,6 +37,10 @@
<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" /> -->

@ -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);
}

@ -4,6 +4,7 @@ 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;
@ -37,11 +38,11 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
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;
@ -50,6 +51,7 @@ using Jellyfin.Api.Helpers;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@ -90,7 +92,6 @@ 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;
@ -98,12 +99,12 @@ using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
{
@ -120,18 +121,23 @@ namespace Emby.Server.Implementations
private readonly IFileSystem _fileSystemManager;
private readonly INetworkManager _networkManager;
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private IHttpServer _httpServer;
private IHttpClient _httpClient;
private IHttpClientFactory _httpClientFactory;
private IWebSocketManager _webSocketManager;
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
@ -235,8 +241,14 @@ namespace Emby.Server.Implementations
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
@ -246,6 +258,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
_jsonSerializer = new JsonSerializer();
ServiceCollection = serviceCollection;
_networkManager = networkManager;
@ -277,6 +291,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)
@ -306,16 +324,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.
@ -446,8 +464,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);
@ -502,9 +519,6 @@ namespace Emby.Server.Implementations
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>
@ -524,8 +538,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>();
ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
ServiceCollection.AddSingleton(_networkManager);
ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
@ -544,8 +556,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IZipClient, ZipClient>();
ServiceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
@ -581,8 +591,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
ServiceCollection.AddSingleton<ServiceController>();
ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
@ -655,8 +664,8 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
_httpServer = Resolve<IHttpServer>();
_httpClient = Resolve<IHttpClient>();
_httpClientFactory = Resolve<IHttpClientFactory>();
_webSocketManager = Resolve<IWebSocketManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@ -757,7 +766,6 @@ namespace Emby.Server.Implementations
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
}
/// <summary>
@ -777,7 +785,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>(),
@ -943,7 +952,7 @@ namespace Emby.Server.Implementations
}
}
if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
@ -1017,6 +1026,119 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
/// <summary>
/// Comparison function used in <see cref="GetPlugins" />.
/// </summary>
/// <param name="a">Item to compare.</param>
/// <param name="b">Item to compare with.</param>
/// <returns>Boolean result of the operation.</returns>
private static int VersionCompare(
(Version PluginVersion, string Name, string Path) a,
(Version PluginVersion, string Name, string Path) b)
{
int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
if (compare == 0)
{
return a.PluginVersion.CompareTo(b.PluginVersion);
}
return compare;
}
/// <summary>
/// Returns a list of plugins to install.
/// </summary>
/// <param name="path">Path to check.</param>
/// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
/// <returns>Enumerable list of dlls to load.</returns>
private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
{
var dllList = new List<string>();
var versions = new List<(Version PluginVersion, string Name, string Path)>();
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
string metafile;
foreach (var dir in directories)
{
try
{
metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = new Version(0, 0, 0, 1);
}
if (!Version.TryParse(manifest.Version, out var version))
{
version = new Version(0, 0, 0, 1);
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
versions.Add((version, manifest.Name, dir));
}
}
else
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
{
// Versioned folder.
versions.Add((ver, metafile, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add((new Version(0, 0, 0, 1), metafile, dir));
}
}
}
catch
{
continue;
}
}
string lastName = string.Empty;
versions.Sort(VersionCompare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
Directory.Delete(versions[x].Path, true);
}
catch (Exception e)
{
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
}
}
}
return dllList;
}
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
@ -1025,7 +1147,7 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
{
Assembly plugAss;
try
@ -1139,7 +1261,8 @@ namespace Emby.Server.Implementations
Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
LocalAddress = localAddress
LocalAddress = localAddress,
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
@ -1301,25 +1424,17 @@ namespace Emby.Server.Implementations
try
{
using (var response = await _httpClient.SendAsync(
new HttpRequestOptions
{
Url = apiUrl,
LogErrorResponseBody = false,
BufferContent = false,
CancellationToken = cancellationToken
}, HttpMethod.Post).ConfigureAwait(false))
{
using (var reader = new StreamReader(response.Content))
{
var result = await reader.ReadToEndAsync().ConfigureAwait(false);
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
return valid;
}
}
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
return valid;
}
catch (OperationCanceledException)
{
@ -1406,7 +1521,7 @@ namespace Emby.Server.Implementations
foreach (var assembly in assemblies)
{
Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName);
yield return assembly;
}
}
@ -1441,10 +1556,6 @@ namespace Emby.Server.Implementations
}
}
public virtual void EnableLoopback(string appName)
{
}
private bool _disposed = false;
/// <summary>

@ -890,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);
}
}

@ -15,10 +15,10 @@ 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.FalseString },
{ BindToUnixSocketKey, bool.FalseString }
};
}

@ -143,8 +143,17 @@ namespace Emby.Server.Implementations.Data
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
=> sql.Select(connection.PrepareStatement);
public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
{
int len = sql.Count;
IStatement[] statements = new IStatement[len];
for (int i = 0; i < len; i++)
{
statements[i] = connection.PrepareStatement(sql[i]);
}
return statements;
}
protected bool TableExists(ManagedConnection connection, string name)
{

@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value.ToByteArray());
Span<byte> byteValue = stackalloc byte[16];
value.TryWriteBytes(byteValue);
bindParam.Bind(byteValue);
}
else
{

@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.Data
"pragma shrink_memory"
};
string[] postQueries =
{
// obsolete
@ -560,7 +559,7 @@ namespace Emby.Server.Implementations.Data
{
SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
}).ToList();
});
using (var saveItemStatement = statements[0])
using (var deleteAncestorsStatement = statements[1])
@ -2925,7 +2924,7 @@ namespace Emby.Server.Implementations.Data
{
connection.RunInTransaction(db =>
{
var statements = PrepareAll(db, statementTexts).ToList();
var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@ -2963,7 +2962,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
using (var statement = statements[statements.Count - 1])
using (var statement = statements[statements.Length - 1])
{
if (EnableJoinUserData(query))
{
@ -3329,7 +3328,7 @@ namespace Emby.Server.Implementations.Data
{
connection.RunInTransaction(db =>
{
var statements = PrepareAll(db, statementTexts).ToList();
var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@ -3355,7 +3354,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
using (var statement = statements[statements.Count - 1])
using (var statement = statements[statements.Length - 1])
{
if (EnableJoinUserData(query))
{
@ -3718,26 +3717,31 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
}
StringBuilder clauseBuilder = new StringBuilder();
const string Or = " OR ";
var trailerTypes = query.TrailerTypes;
int trailerTypesLen = trailerTypes.Length;
if (trailerTypesLen > 0)
{
const string Or = " OR ";
StringBuilder clause = new StringBuilder("(", trailerTypesLen * 32);
clauseBuilder.Append('(');
for (int i = 0; i < trailerTypesLen; i++)
{
var paramName = "@TrailerTypes" + i;
clause.Append("TrailerTypes like ")
clauseBuilder.Append("TrailerTypes like ")
.Append(paramName)
.Append(Or);
statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
}
// Remove last " OR "
clause.Length -= Or.Length;
clause.Append(')');
clauseBuilder.Length -= Or.Length;
clauseBuilder.Append(')');
whereClauses.Add(clause.ToString());
whereClauses.Add(clauseBuilder.ToString());
clauseBuilder.Length = 0;
}
if (query.IsAiring.HasValue)
@ -3757,23 +3761,35 @@ namespace Emby.Server.Implementations.Data
}
}
if (query.PersonIds.Length > 0)
int personIdsLen = query.PersonIds.Length;
if (personIdsLen > 0)
{
// TODO: Should this query with CleanName ?
var clauses = new List<string>();
var index = 0;
foreach (var personId in query.PersonIds)
clauseBuilder.Append('(');
Span<byte> idBytes = stackalloc byte[16];
for (int i = 0; i < personIdsLen; i++)
{
var paramName = "@PersonId" + index;
string paramName = "@PersonId" + i;
clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=")
.Append(paramName)
.Append("))) OR ");
clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))");
statement?.TryBind(paramName, personId.ToByteArray());
index++;
if (statement != null)
{
query.PersonIds[i].TryWriteBytes(idBytes);
statement.TryBind(paramName, idBytes);
}
}
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
// Remove last " OR "
clauseBuilder.Length -= Or.Length;
clauseBuilder.Append(')');
whereClauses.Add(clauseBuilder.ToString());
clauseBuilder.Length = 0;
}
if (!string.IsNullOrWhiteSpace(query.Person))
@ -5149,7 +5165,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
var itemIdBlob = itemId.ToByteArray();
Span<byte> itemIdBlob = stackalloc byte[16];
itemId.TryWriteBytes(itemIdBlob);
// First delete
deleteAncestorsStatement.Reset();
@ -5165,17 +5182,15 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
for (var i = 0; i < ancestorIds.Count; i++)
{
if (i > 0)
{
insertText.Append(',');
}
insertText.AppendFormat(
CultureInfo.InvariantCulture,
"(@ItemId, @AncestorId{0}, @AncestorIdText{0})",
"(@ItemId, @AncestorId{0}, @AncestorIdText{0}),",
i.ToString(CultureInfo.InvariantCulture));
}
// Remove last ,
insertText.Length--;
using (var statement = PrepareStatement(db, insertText.ToString()))
{
statement.TryBind("@ItemId", itemIdBlob);
@ -5185,8 +5200,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var index = i.ToString(CultureInfo.InvariantCulture);
var ancestorId = ancestorIds[i];
ancestorId.TryWriteBytes(itemIdBlob);
statement.TryBind("@AncestorId" + index, ancestorId.ToByteArray());
statement.TryBind("@AncestorId" + index, itemIdBlob);
statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
}
@ -5466,7 +5482,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts).ToList();
var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@ -5517,7 +5533,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
+ GetJoinUserDataText(query)
+ whereText;
using (var statement = statements[statements.Count - 1])
using (var statement = statements[statements.Length - 1])
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))

@ -197,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);
}
}
@ -1431,7 +1431,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
return width / height;
return (double)width / height;
}
}
}

@ -32,10 +32,10 @@
<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="Microsoft.Extensions.DependencyInjection" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" />
<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" />

@ -1,335 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpClientManager
{
/// <summary>
/// Class HttpClientManager.
/// </summary>
public class HttpClientManager : IHttpClient
{
private readonly ILogger<HttpClientManager> _logger;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IApplicationHost _appHost;
/// <summary>
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
/// DON'T dispose it after use.
/// </summary>
/// <value>The HTTP clients.</value>
private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientManager" /> class.
/// </summary>
public HttpClientManager(
IApplicationPaths appPaths,
ILogger<HttpClientManager> logger,
IFileSystem fileSystem,
IApplicationHost appHost)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_fileSystem = fileSystem;
_appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths));
_appHost = appHost;
}
/// <summary>
/// Gets the correct http client for the given url.
/// </summary>
/// <param name="url">The url.</param>
/// <returns>HttpClient.</returns>
private HttpClient GetHttpClient(string url)
{
var key = GetHostFromUrl(url);
if (!_httpClients.TryGetValue(key, out var client))
{
client = new HttpClient()
{
BaseAddress = new Uri(url)
};
_httpClients.TryAdd(key, client);
}
return client;
}
private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
{
string url = options.Url;
var uriAddress = new Uri(url);
string userInfo = uriAddress.UserInfo;
if (!string.IsNullOrWhiteSpace(userInfo))
{
_logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal);
}
var request = new HttpRequestMessage(method, url);
foreach (var header in options.RequestHeaders)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (options.EnableDefaultUserAgent
&& !request.Headers.TryGetValues(HeaderNames.UserAgent, out _))
{
request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent);
}
switch (options.DecompressionMethod)
{
case CompressionMethods.Deflate | CompressionMethods.Gzip:
request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
break;
case CompressionMethods.Deflate:
request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
break;
case CompressionMethods.Gzip:
request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
break;
default:
break;
}
if (options.EnableKeepAlive)
{
request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
}
// request.Headers.Add(HeaderNames.CacheControl, "no-cache");
/*
if (!string.IsNullOrWhiteSpace(userInfo))
{
var parts = userInfo.Split(':');
if (parts.Length == 2)
{
request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
}
}
*/
return request;
}
/// <summary>
/// Gets the response internal.
/// </summary>
/// <param name="options">The options.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
=> SendAsync(options, HttpMethod.Get);
/// <summary>
/// Performs a GET request and returns the resulting stream.
/// </summary>
/// <param name="options">The options.</param>
/// <returns>Task{Stream}.</returns>
public async Task<Stream> Get(HttpRequestOptions options)
{
var response = await GetResponse(options).ConfigureAwait(false);
return response.Content;
}
/// <summary>
/// send as an asynchronous operation.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="httpMethod">The HTTP method.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
=> SendAsync(options, new HttpMethod(httpMethod));
/// <summary>
/// send as an asynchronous operation.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="httpMethod">The HTTP method.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
{
if (options.CacheMode == CacheMode.None)
{
return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
}
var url = options.Url;
var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
var response = GetCachedResponse(responseCachePath, options.CacheLength, url);
if (response != null)
{
return response;
}
response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK)
{
await CacheResponse(response, responseCachePath).ConfigureAwait(false);
}
return response;
}
private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url)
{
if (File.Exists(responseCachePath)
&& _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
{
var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
return new HttpResponseInfo
{
ResponseUrl = url,
Content = stream,
StatusCode = HttpStatusCode.OK,
ContentLength = stream.Length
};
}
return null;
}
private async Task CacheResponse(HttpResponseInfo response, string responseCachePath)
{
Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath));
using (var fileStream = new FileStream(
responseCachePath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
IODefaults.FileStreamBufferSize,
true))
{
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
response.Content.Position = 0;
}
}
private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
{
ValidateParams(options);
options.CancellationToken.ThrowIfCancellationRequested();
var client = GetHttpClient(options.Url);
var httpWebRequest = GetRequestMessage(options, httpMethod);
if (!string.IsNullOrEmpty(options.RequestContent)
|| httpMethod == HttpMethod.Post)
{
if (options.RequestContent != null)
{
httpWebRequest.Content = new StringContent(
options.RequestContent,
null,
options.RequestContentType);
}
else
{
httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
}
}
options.CancellationToken.ThrowIfCancellationRequested();
var response = await client.SendAsync(
httpWebRequest,
options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
options.CancellationToken).ConfigureAwait(false);
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
options.CancellationToken.ThrowIfCancellationRequested();
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return new HttpResponseInfo(response.Headers, response.Content.Headers)
{
Content = stream,
StatusCode = response.StatusCode,
ContentType = response.Content.Headers.ContentType?.MediaType,
ContentLength = response.Content.Headers.ContentLength,
ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
};
}
/// <inheritdoc />
public Task<HttpResponseInfo> Post(HttpRequestOptions options)
=> SendAsync(options, HttpMethod.Post);
private void ValidateParams(HttpRequestOptions options)
{
if (string.IsNullOrEmpty(options.Url))
{
throw new ArgumentNullException(nameof(options));
}
}
/// <summary>
/// Gets the host from URL.
/// </summary>
/// <param name="url">The URL.</param>
/// <returns>System.String.</returns>
private static string GetHostFromUrl(string url)
{
var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
url = url.Substring(index + 3);
var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(host))
{
return host;
}
}
return url;
}
private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
{
if (response.IsSuccessStatusCode)
{
return;
}
if (options.LogErrorResponseBody)
{
string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
_logger.LogError("HTTP request failed with message: {Message}", msg);
}
throw new HttpException(response.ReasonPhrase)
{
StatusCode = response.StatusCode
};
}
}
}

@ -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 Jellyfin.Data.Events;
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.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);
using 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());
}
}
}

@ -1,721 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Services;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IRequest = MediaBrowser.Model.Services.IRequest;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
namespace Emby.Server.Implementations.HttpServer
{
/// <summary>
/// Class HttpResultFactory.
/// </summary>
public class HttpResultFactory : IHttpResultFactory
{
// Last-Modified and If-Modified-Since must follow strict date format,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
// We specifically use en-US culture because both day of week and month names require it
private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<HttpResultFactory> _logger;
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStreamHelper _streamHelper;
/// <summary>
/// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
/// </summary>
public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
{
_fileSystem = fileSystem;
_jsonSerializer = jsonSerializer;
_streamHelper = streamHelper;
_logger = loggerfactory.CreateLogger<HttpResultFactory>();
}
/// <summary>
/// Gets the result.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <param name="content">The content.</param>
/// <param name="contentType">Type of the content.</param>
/// <param name="responseHeaders">The response headers.</param>
/// <returns>System.Object.</returns>
public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(null, content, contentType, true, responseHeaders);
}
public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetRedirectResult(string url)
{
var responseHeaders = new Dictionary<string, string>();
responseHeaders[HeaderNames.Location] = url;
var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the HTTP result.
/// </summary>
private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
{
var result = new StreamWriter(content, contentType);
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the HTTP result.
/// </summary>
private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
{
string compressionType = null;
bool isHeadRequest = false;
if (requestContext != null)
{
compressionType = GetCompressionType(requestContext, content, contentType);
isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
}
IHasHeaders result;
if (string.IsNullOrEmpty(compressionType))
{
var contentLength = content.Length;
if (isHeadRequest)
{
content = Array.Empty<byte>();
}
result = new StreamWriter(content, contentType, contentLength);
}
else
{
result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the HTTP result.
/// </summary>
private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
{
IHasHeaders result;
var bytes = Encoding.UTF8.GetBytes(content);
var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(compressionType))
{
var contentLength = bytes.Length;
if (isHeadRequest)
{
bytes = Array.Empty<byte>();
}
result = new StreamWriter(bytes, contentType, contentLength);
}
else
{
result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the optimized result.
/// </summary>
/// <typeparam name="T"></typeparam>
public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
where T : class
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
responseHeaders[HeaderNames.Expires] = "0";
return ToOptimizedResultInternal(requestContext, result, responseHeaders);
}
private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
{
if (responseContentType == null)
{
return null;
}
// Per apple docs, hls manifests must be compressed
if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
{
return null;
}
if (content.Length < 1024)
{
return null;
}
return GetCompressionType(request);
}
private static string GetCompressionType(IRequest request)
{
var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
if (!string.IsNullOrEmpty(acceptEncoding))
{
// if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
// return "br";
if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
{
return "deflate";
}
if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
{
return "gzip";
}
}
return null;
}
/// <summary>
/// Returns the optimized result for the IRequestContext.
/// Does not use or store results in any cache.
/// </summary>
/// <param name="request"></param>
/// <param name="dto"></param>
/// <returns></returns>
public object ToOptimizedResult<T>(IRequest request, T dto)
{
return ToOptimizedResultInternal(request, dto);
}
private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
{
// TODO: @bond use Span and .Equals
var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
switch (contentType)
{
case "application/xml":
case "text/xml":
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
case "application/json":
case "text/json":
return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
default:
break;
}
var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
var ms = new MemoryStream();
var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
writerFn(dto, ms);
ms.Position = 0;
if (isHeadRequest)
{
using (ms)
{
return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
}
}
return GetHttpResult(request, ms, contentType, true, responseHeaders);
}
private IHasHeaders GetCompressedResult(
byte[] content,
string requestedCompressionType,
IDictionary<string, string> responseHeaders,
bool isHeadRequest,
string contentType)
{
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
content = Compress(content, requestedCompressionType);
responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
var contentLength = content.Length;
if (isHeadRequest)
{
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
AddResponseHeaders(result, responseHeaders);
return result;
}
else
{
var result = new StreamWriter(content, contentType, contentLength);
AddResponseHeaders(result, responseHeaders);
return result;
}
}
private byte[] Compress(byte[] bytes, string compressionType)
{
if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
{
return Deflate(bytes);
}
if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
{
return GZip(bytes);
}
throw new NotSupportedException(compressionType);
}
private static byte[] Deflate(byte[] bytes)
{
// In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
// Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
using (var ms = new MemoryStream())
using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
{
zipStream.Write(bytes, 0, bytes.Length);
zipStream.Dispose();
return ms.ToArray();
}
}
private static byte[] GZip(byte[] buffer)
{
using (var ms = new MemoryStream())
using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
{
zipStream.Write(buffer, 0, buffer.Length);
zipStream.Dispose();
return ms.ToArray();
}
}
private static string SerializeToXmlString(object from)
{
using (var ms = new MemoryStream())
{
var xwSettings = new XmlWriterSettings();
xwSettings.Encoding = new UTF8Encoding(false);
xwSettings.OmitXmlDeclaration = false;
using (var xw = XmlWriter.Create(ms, xwSettings))
{
var serializer = new DataContractSerializer(from.GetType());
serializer.WriteObject(xw, from);
xw.Flush();
ms.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(ms))
{
return reader.ReadToEnd();
}
}
}
}
/// <summary>
/// Pres the process optimized result.
/// </summary>
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
{
bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
if (!noCache)
{
if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
{
_logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
return null;
}
if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
{
AddAgeHeader(responseHeaders, options.DateLastModified);
var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
AddResponseHeaders(result, responseHeaders);
return result;
}
}
return null;
}
public Task<object> GetStaticFileResult(IRequest requestContext,
string path,
FileShare fileShare = FileShare.Read)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
return GetStaticFileResult(requestContext, new StaticFileResultOptions
{
Path = path,
FileShare = fileShare
});
}
public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
{
var path = options.Path;
var fileShare = options.FileShare;
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("Path can't be empty.", nameof(options));
}
if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
{
throw new ArgumentException("FileShare must be either Read or ReadWrite");
}
if (string.IsNullOrEmpty(options.ContentType))
{
options.ContentType = MimeTypes.GetMimeType(path);
}
if (!options.DateLastModified.HasValue)
{
options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
}
options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
return GetStaticResult(requestContext, options);
}
/// <summary>
/// Gets the file stream.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="fileShare">The file share.</param>
/// <returns>Stream.</returns>
private Stream GetFileStream(string path, FileShare fileShare)
{
return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
}
public Task<object> GetStaticResult(IRequest requestContext,
Guid cacheKey,
DateTime? lastDateModified,
TimeSpan? cacheDuration,
string contentType,
Func<Task<Stream>> factoryFn,
IDictionary<string, string> responseHeaders = null,
bool isHeadRequest = false)
{
return GetStaticResult(requestContext, new StaticResultOptions
{
CacheDuration = cacheDuration,
ContentFactory = factoryFn,
ContentType = contentType,
DateLastModified = lastDateModified,
IsHeadRequest = isHeadRequest,
ResponseHeaders = responseHeaders
});
}
public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
{
options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var contentType = options.ContentType;
if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
{
// See if the result is already cached in the browser
var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
if (result != null)
{
return result;
}
}
// TODO: We don't really need the option value
var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
var factoryFn = options.ContentFactory;
var responseHeaders = options.ResponseHeaders;
AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
AddAgeHeader(responseHeaders, options.DateLastModified);
var rangeHeader = requestContext.Headers[HeaderNames.Range];
if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
{
var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
{
OnComplete = options.OnComplete,
OnError = options.OnError,
FileShare = options.FileShare
};
AddResponseHeaders(hasHeaders, options.ResponseHeaders);
return hasHeaders;
}
var stream = await factoryFn().ConfigureAwait(false);
var totalContentLength = options.ContentLength;
if (!totalContentLength.HasValue)
{
try
{
totalContentLength = stream.Length;
}
catch (NotSupportedException)
{
}
}
if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
{
var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
{
OnComplete = options.OnComplete
};
AddResponseHeaders(hasHeaders, options.ResponseHeaders);
return hasHeaders;
}
else
{
if (totalContentLength.HasValue)
{
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
}
if (isHeadRequest)
{
using (stream)
{
return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
}
}
var hasHeaders = new StreamWriter(stream, contentType)
{
OnComplete = options.OnComplete,
OnError = options.OnError
};
AddResponseHeaders(hasHeaders, options.ResponseHeaders);
return hasHeaders;
}
}
/// <summary>
/// Adds the caching responseHeaders.
/// </summary>
private void AddCachingHeaders(
IDictionary<string, string> responseHeaders,
TimeSpan? cacheDuration,
bool noCache,
DateTime? lastModifiedDate)
{
if (noCache)
{
responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
return;
}
if (cacheDuration.HasValue)
{
responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
}
else
{
responseHeaders[HeaderNames.CacheControl] = "public";
}
if (lastModifiedDate.HasValue)
{
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
}
}
/// <summary>
/// Adds the age header.
/// </summary>
/// <param name="responseHeaders">The responseHeaders.</param>
/// <param name="lastDateModified">The last date modified.</param>
private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
{
if (lastDateModified.HasValue)
{
responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
}
}
/// <summary>
/// Determines whether [is not modified] [the specified if modified since].
/// </summary>
/// <param name="ifModifiedSince">If modified since.</param>
/// <param name="cacheDuration">Duration of the cache.</param>
/// <param name="dateModified">The date modified.</param>
/// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
{
if (dateModified.HasValue)
{
var lastModified = NormalizeDateForComparison(dateModified.Value);
ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
return lastModified <= ifModifiedSince;
}
if (cacheDuration.HasValue)
{
var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
if (DateTime.UtcNow < cacheExpirationDate)
{
return true;
}
}
return false;
}
/// <summary>
/// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that.
/// </summary>
/// <param name="date">The date.</param>
/// <returns>DateTime.</returns>
private static DateTime NormalizeDateForComparison(DateTime date)
{
return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
}
/// <summary>
/// Adds the response headers.
/// </summary>
/// <param name="hasHeaders">The has options.</param>
/// <param name="responseHeaders">The response headers.</param>
private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
{
foreach (var item in responseHeaders)
{
hasHeaders.Headers[item.Key] = item.Value;
}
}
}
}

@ -1,212 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
{
private const int BufferSize = 81920;
private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
private List<KeyValuePair<long, long?>> _requestedRanges;
/// <summary>
/// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
/// </summary>
/// <param name="rangeHeader">The range header.</param>
/// <param name="contentLength">The content length.</param>
/// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param>
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
RangeHeader = rangeHeader;
SourceStream = source;
IsHeadRequest = isHeadRequest;
ContentType = contentType;
Headers[HeaderNames.ContentType] = contentType;
Headers[HeaderNames.AcceptRanges] = "bytes";
StatusCode = HttpStatusCode.PartialContent;
SetRangeValues(contentLength);
}
/// <summary>
/// Gets or sets the source stream.
/// </summary>
/// <value>The source stream.</value>
private Stream SourceStream { get; set; }
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; }
private long TotalContentLength { get; set; }
public Action OnComplete { get; set; }
/// <summary>
/// Additional HTTP Headers
/// </summary>
/// <value>The headers.</value>
public IDictionary<string, string> Headers => _options;
/// <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], CultureInfo.InvariantCulture);
}
if (!string.IsNullOrEmpty(vals[1]))
{
end = long.Parse(vals[1], CultureInfo.InvariantCulture);
}
_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(long contentLength)
{
var requestedRange = RequestedRanges[0];
TotalContentLength = contentLength;
// 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;
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
if (RangeStart > 0 && SourceStream.CanSeek)
{
SourceStream.Position = RangeStart;
}
}
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{
try
{
// Headers only
if (IsHeadRequest)
{
return;
}
using (var source = SourceStream)
{
// If the requested range is "0-", we can optimize by just doing a stream copy
if (RangeEnd >= TotalContentLength - 1)
{
await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
}
else
{
await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
}
}
}
finally
{
OnComplete?.Invoke();
}
}
private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{
var array = ArrayPool<byte>.Shared.Rent(BufferSize);
try
{
int bytesRead;
while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
var bytesToCopy = Math.Min(bytesRead, copyLength);
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
copyLength -= bytesToCopy;
if (copyLength <= 0)
{
break;
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
}
}
}

@ -1,113 +0,0 @@
using System;
using System.Globalization;
using System.Text;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
/// <summary>
/// Class ResponseFilter.
/// </summary>
public class ResponseFilter
{
private readonly IHttpServer _server;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ResponseFilter"/> class.
/// </summary>
/// <param name="server">The HTTP server.</param>
/// <param name="logger">The logger.</param>
public ResponseFilter(IHttpServer server, ILogger logger)
{
_server = server;
_logger = logger;
}
/// <summary>
/// Filters the response.
/// </summary>
/// <param name="req">The req.</param>
/// <param name="res">The res.</param>
/// <param name="dto">The dto.</param>
public void FilterResponse(IRequest req, HttpResponse res, object dto)
{
foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
{
res.Headers.Add(key, value);
}
// Try to prevent compatibility view
res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " +
"Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
"Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
"Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
"X-Emby-Authorization";
if (dto is Exception exception)
{
_logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl);
if (!string.IsNullOrEmpty(exception.Message))
{
var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
error = RemoveControlCharacters(error);
res.Headers.Add("X-Application-Error-Code", error);
}
}
if (dto is IHasHeaders hasHeaders)
{
if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
{
hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
}
// Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength)
&& !string.IsNullOrEmpty(contentLength))
{
var length = long.Parse(contentLength, CultureInfo.InvariantCulture);
if (length > 0)
{
res.ContentLength = length;
}
}
}
}
/// <summary>
/// Removes the control characters.
/// </summary>
/// <param name="inString">The in string.</param>
/// <returns>System.String.</returns>
public static string RemoveControlCharacters(string inString)
{
if (inString == null)
{
return null;
}
else if (inString.Length == 0)
{
return inString;
}
var newString = new StringBuilder(inString.Length);
foreach (var ch in inString)
{
if (!char.IsControl(ch))
{
newString.Append(ch);
}
}
return newString.ToString();
}
}
}

@ -1,17 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Linq;
using Emby.Server.Implementations.SocketSharp;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
public class AuthService : IAuthService
{
private readonly IAuthorizationContext _authorizationContext;
private readonly ISessionManager _sessionManager;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
public AuthService(
IAuthorizationContext authorizationContext,
IServerConfigurationManager config,
ISessionManager sessionManager,
INetworkManager networkManager)
IAuthorizationContext authorizationContext)
{
_authorizationContext = authorizationContext;
_config = config;
_sessionManager = sessionManager;
_networkManager = networkManager;
}
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{
ValidateUser(request, authAttributes);
}
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
{
var req = new WebSocketSharpRequest(request, null, request.Path);
var user = ValidateUser(req, authAttributes);
return user;
}
public AuthorizationInfo Authenticate(HttpRequest request)
@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security
return auth;
}
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{
// This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(authAttributes, request))
{
ValidateSecurityToken(request, auth.Token);
}
if (authAttributes.AllowLocalOnly && !request.IsLocal)
{
throw new SecurityException("Operation not found.");
}
var user = auth.User;
if (user == null && auth.UserId != Guid.Empty)
{
throw new AuthenticationException("User with Id " + auth.UserId + " not found");
}
if (user != null)
{
ValidateUserAccess(user, request, authAttributes);
}
var info = GetTokenInfo(request);
if (!IsExemptFromRoles(auth, authAttributes, request, info))
{
var roles = authAttributes.GetRoles();
ValidateRoles(roles, user);
}
if (!string.IsNullOrEmpty(auth.DeviceId) &&
!string.IsNullOrEmpty(auth.Client) &&
!string.IsNullOrEmpty(auth.Device))
{
_sessionManager.LogSessionActivity(
auth.Client,
auth.Version,
auth.DeviceId,
auth.Device,
request.RemoteIp,
user);
}
return user;
}
private void ValidateUserAccess(
User user,
IRequest request,
IAuthenticationAttributes authAttributes)
{
if (user.HasPermission(PermissionKind.IsDisabled))
{
throw new SecurityException("User account has been disabled.");
}
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
{
throw new SecurityException("User account has been disabled.");
}
if (!user.HasPermission(PermissionKind.IsAdministrator)
&& !authAttributes.EscapeParentalControl
&& !user.IsParentalScheduleAllowed())
{
request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
throw new SecurityException("This user account is not allowed access at this time.");
}
}
private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
{
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
{
return true;
}
if (authAttribtues.AllowLocal && request.IsLocal)
{
return true;
}
if (authAttribtues.AllowLocalOnly && request.IsLocal)
{
return true;
}
if (authAttribtues.IgnoreLegacyAuth)
{
return true;
}
return false;
}
private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo)
{
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
{
return true;
}
if (authAttribtues.AllowLocal && request.IsLocal)
{
return true;
}
if (authAttribtues.AllowLocalOnly && request.IsLocal)
{
return true;
}
if (string.IsNullOrEmpty(auth.Token))
{
return true;
}
if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
{
return true;
}
return false;
}
private static void ValidateRoles(string[] roles, User user)
{
if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
{
throw new SecurityException("User does not have admin access.");
}
}
if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
{
throw new SecurityException("User does not have delete access.");
}
}
if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
{
throw new SecurityException("User does not have download access.");
}
}
}
private static AuthenticationInfo GetTokenInfo(IRequest request)
{
request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
return info as AuthenticationInfo;
}
private void ValidateSecurityToken(IRequest request, string token)
{
if (string.IsNullOrEmpty(token))
{
throw new AuthenticationException("Access token is required.");
}
var info = GetTokenInfo(request);
if (info == null)
{
throw new AuthenticationException("Access token is invalid or expired.");
}
}
}
}

@ -7,7 +7,6 @@ using System.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_userManager = userManager;
}
public AuthorizationInfo GetAuthorizationInfo(object requestContext)
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
{
return GetAuthorizationInfo((IRequest)requestContext);
}
public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
{
if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached))
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
return (AuthorizationInfo)cached;
}
@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private AuthorizationInfo GetAuthorization(IRequest httpReq)
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var (authInfo, originalAuthInfo) =
GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
if (originalAuthInfo != null)
{
httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
}
httpReq.Items["AuthorizationInfo"] = authInfo;
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Headers[HeaderNames.Authorization];
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);

@ -2,11 +2,11 @@
using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
{
@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
public SessionInfo GetSession(IRequest requestContext)
public SessionInfo GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
}
private AuthenticationInfo GetTokenInfo(IRequest request)
{
request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
return info as AuthenticationInfo;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
}
public SessionInfo GetSession(object requestContext)
{
return GetSession((IRequest)requestContext);
return GetSession((HttpContext)requestContext);
}
public User GetUser(IRequest requestContext)
public User GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
return GetUser((IRequest)requestContext);
return GetUser((HttpContext)requestContext);
}
}
}

@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
/// <summary>
/// Class StreamWriter.
/// </summary>
public class StreamWriter : IAsyncStreamWriter, IHasHeaders
{
/// <summary>
/// The options.
/// </summary>
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
/// <summary>
/// Initializes a new instance of the <see cref="StreamWriter" /> class.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param>
public StreamWriter(Stream source, string contentType)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
SourceStream = source;
Headers["Content-Type"] = contentType;
if (source.CanSeek)
{
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
}
Headers[HeaderNames.ContentType] = contentType;
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamWriter"/> class.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param>
/// <param name="contentLength">The content length.</param>
public StreamWriter(byte[] source, string contentType, int contentLength)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
SourceBytes = source;
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentType] = contentType;
}
/// <summary>
/// Gets or sets the source stream.
/// </summary>
/// <value>The source stream.</value>
private Stream SourceStream { get; set; }
private byte[] SourceBytes { get; set; }
/// <summary>
/// Gets the options.
/// </summary>
/// <value>The options.</value>
public IDictionary<string, string> Headers => _options;
/// <summary>
/// Fires when complete.
/// </summary>
public Action OnComplete { get; set; }
/// <summary>
/// Fires when an error occours.
/// </summary>
public Action OnError { get; set; }
/// <inheritdoc />
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{
try
{
var bytes = SourceBytes;
if (bytes != null)
{
await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
}
else
{
using (var src = SourceStream)
{
await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
}
}
}
catch
{
OnError?.Invoke();
throw;
}
finally
{
OnComplete?.Invoke();
}
}
}
}

@ -0,0 +1,102 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer
{
public class WebSocketManager : IWebSocketManager
{
private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private bool _disposed = false;
public WebSocketManager(
ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory)
{
_logger = logger;
_loggerFactory = loggerFactory;
}
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
/// <inheritdoc />
public 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);
using 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>
/// Adds the rest handlers.
/// </summary>
/// <param name="listeners">The web socket listeners.</param>
public void Init(IEnumerable<IWebSocketListener> listeners)
{
_webSocketListeners = listeners.ToArray();
}
/// <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());
}
}
}

@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO
continue;
}
_logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
_logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
try
{
@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO
// For now swallow and log.
// Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
// Should we remove it from it's parent?
_logger.LogError(ex, "Error refreshing {name}", item.Name);
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {name}", item.Name);
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
}
}
@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO
}
}
/// <inheritdoc />
public void Dispose()
{
_disposed = true;

@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path);
_logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
}
}
}

@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO
}
}
public virtual void SetReadOnly(string path, bool isReadOnly)
{
if (OperatingSystem.Id != OperatingSystemId.Windows)
{
return;
}
var info = GetExtendedFileSystemInfo(path);
if (info.Exists && info.IsReadOnly != isReadOnly)
{
if (isReadOnly)
{
File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly);
}
else
{
var attributes = File.GetAttributes(path);
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
File.SetAttributes(path, attributes);
}
}
}
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
{
if (OperatingSystem.Id != OperatingSystemId.Windows)
@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO
return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
}
public virtual void SetExecutable(string path)
{
if (OperatingSystem.Id == OperatingSystemId.Darwin)
{
RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path));
}
}
private static void RunProcess(string path, string args, string workingDirectory)
{
using (var process = Process.Start(new ProcessStartInfo

@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO
{
public class StreamHelper : IStreamHelper
{
private const int StreamCopyToBufferSize = 81920;
public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO
}
}
public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
try
{
int totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
var bytesToWrite = bytesRead;
if (bytesToWrite > 0)
{
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
}
}
return totalBytesRead;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
try
{
int bytesRead;

@ -513,10 +513,11 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(type));
}
if (key.StartsWith(_configurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal))
string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
if (key.StartsWith(programDataPath, StringComparison.Ordinal))
{
// Try to normalize paths located underneath program-data in an attempt to make them more portable
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
key = key.Substring(programDataPath.Length)
.TrimStart('/', '\\')
.Replace('/', '\\');
}
@ -871,17 +872,17 @@ namespace Emby.Server.Implementations.Library
public Guid GetStudioId(string name)
{
return GetItemByNameId<Studio>(Studio.GetPath, name);
return GetItemByNameId<Studio>(Studio.GetPath(name));
}
public Guid GetGenreId(string name)
{
return GetItemByNameId<Genre>(Genre.GetPath, name);
return GetItemByNameId<Genre>(Genre.GetPath(name));
}
public Guid GetMusicGenreId(string name)
{
return GetItemByNameId<MusicGenre>(MusicGenre.GetPath, name);
return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
}
/// <summary>
@ -943,7 +944,7 @@ namespace Emby.Server.Implementations.Library
{
var existing = GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(T).Name },
IncludeItemTypes = new[] { nameof(MusicArtist) },
Name = name,
DtoOptions = options
}).Cast<MusicArtist>()
@ -957,13 +958,11 @@ namespace Emby.Server.Implementations.Library
}
}
var id = GetItemByNameId<T>(getPathFn, name);
var path = getPathFn(name);
var id = GetItemByNameId<T>(path);
var item = GetItemById(id) as T;
if (item == null)
{
var path = getPathFn(name);
item = new T
{
Name = name,
@ -979,10 +978,9 @@ namespace Emby.Server.Implementations.Library
return item;
}
private Guid GetItemByNameId<T>(Func<string, string> getPathFn, string name)
private Guid GetItemByNameId<T>(string path)
where T : BaseItem, new()
{
var path = getPathFn(name);
var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
@ -1805,21 +1803,18 @@ namespace Emby.Server.Implementations.Library
/// <param name="items">The items.</param>
/// <param name="parent">The parent item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
{
// Don't iterate multiple times
var itemsList = items.ToList();
_itemRepository.SaveItems(itemsList, cancellationToken);
_itemRepository.SaveItems(items, cancellationToken);
foreach (var item in itemsList)
foreach (var item in items)
{
RegisterItem(item);
}
if (ItemAdded != null)
{
foreach (var item in itemsList)
foreach (var item in items)
{
// With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library)

@ -16,13 +16,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class DirectRecorder : IRecorder
{
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IStreamHelper _streamHelper;
public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper)
public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper)
{
_logger = logger;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_streamHelper = streamHelper;
}
@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
// The media source is infinite so we need to handle stopping ourselves
var durationToken = new CancellationTokenSource(duration);
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
}
_logger.LogInformation("Recording completed to file {0}", targetFile);
@ -63,37 +63,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
var httpRequestOptions = new HttpRequestOptions
{
Url = mediaSource.Path,
BufferContent = false,
// Some remote urls will expect a user agent to be supplied
UserAgent = "Emby/3.0",
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
// Shouldn't matter but may cause issues
DecompressionMethod = CompressionMethods.None
};
_logger.LogInformation("Opened recording stream from tuner provider");
using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false))
{
_logger.LogInformation("Opened recording stream from tuner provider");
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{
onStarted();
onStarted();
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
// The media source if infinite so we need to handle stopping ourselves
var durationToken = new CancellationTokenSource(duration);
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
// The media source if infinite so we need to handle stopping ourselves
var durationToken = new CancellationTokenSource(duration);
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false);
}
}
await _streamHelper.CopyUntilCancelled(
await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
output,
IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Recording completed to file {0}", targetFile);
}

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerApplicationHost _appHost;
private readonly ILogger<EmbyTV> _logger;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IJsonSerializer _jsonSerializer;
@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IMediaSourceManager mediaSourceManager,
ILogger<EmbyTV> logger,
IJsonSerializer jsonSerializer,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
ILiveTvManager liveTvManager,
IFileSystem fileSystem,
@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_appHost = appHost;
_logger = logger;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_config = config;
_fileSystem = fileSystem;
_libraryManager = libraryManager;
@ -604,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return Task.CompletedTask;
}
public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
throw new NotImplementedException();
@ -808,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return null;
}
public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings()
{
return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested);
}
public ActiveRecordingInfo GetActiveRecordingInfo(string path)
{
if (string.IsNullOrWhiteSpace(path))
@ -1015,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new Exception("Tuner not found.");
}
private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
{
var json = _jsonSerializer.SerializeToString(mediaSource);
mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id;
return mediaSource;
}
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(channelId))
@ -1654,10 +1635,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
}
return new DirectRecorder(_logger, _httpClient, _streamHelper);
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
}
private void OnSuccessfulRecording(TimerInfo timer, string path)

@ -8,12 +8,9 @@ using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths;
private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
private Process _process;
private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly IServerConfigurationManager _config;
public EncodedRecorder(
ILogger logger,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
IJsonSerializer json,
IServerConfigurationManager config)
IJsonSerializer json)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
_json = json;
_config = config;
}
private static bool CopySubtitles => false;
@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
// The media source is infinite so we need to handle stopping ourselves
var durationToken = new CancellationTokenSource(duration);
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false);
await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false);
_logger.LogInformation("Recording completed to file {0}", targetFile);
}
private EncodingOptions GetEncodingOptions()
{
return _config.GetConfiguration<EncodingOptions>("encoding");
}
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
StartInfo = processStartInfo,
EnableRaisingEvents = true
};
_process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
_process.Exited += (sender, args) => OnFfMpegProcessExited(_process);
_process.Start();
@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
protected string GetOutputSizeParam()
{
var filters = new List<string>();
filters.Add("yadif=0:-1:0");
var output = string.Empty;
if (filters.Count > 0)
{
output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray()));
}
return output;
}
=> "-vf \"yadif=0:-1:0\"";
private void Stop()
{
@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Processes the exited.
/// </summary>
private void OnFfMpegProcessExited(Process process, string inputFile)
private void OnFfMpegProcessExited(Process process)
{
using (process)
{

@ -8,6 +8,8 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
public class SchedulesDirect : IListingsProvider
{
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
private readonly ILogger<SchedulesDirect> _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
IApplicationHost appHost)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
}
@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
while (start <= end)
{
dates.Add(start.ToString("yyyy-MM-dd"));
dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
start = start.AddDays(1);
}
@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var requestString = _jsonSerializer.SerializeToString(requestList);
_logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
var httpOptions = new HttpRequestOptions()
{
Url = ApiUrl + "/schedules",
UserAgent = UserAgent,
CancellationToken = cancellationToken,
LogErrorResponseBody = true,
RequestContent = requestString
};
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
httpOptions.RequestHeaders["token"] = token;
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
using (var response = await Post(httpOptions, true, info).ConfigureAwait(false))
{
var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
httpOptions = new HttpRequestOptions()
{
Url = ApiUrl + "/programs",
UserAgent = UserAgent,
CancellationToken = cancellationToken,
LogErrorResponseBody = true
};
httpOptions.RequestHeaders["token"] = token;
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]";
var programIdsWithImages =
programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
.ToList();
using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false))
{
var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
var programIdsWithImages =
programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
.ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
var programsInfo = new List<ProgramInfo>();
foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
{
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
// " which corresponds to channel " + channelNumber + " and program id " +
// schedule.programID + " which says it has images? " +
// programDict[schedule.programID].hasImageArtwork);
var programsInfo = new List<ProgramInfo>();
foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
if (images != null)
{
var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
if (imageIndex > -1)
{
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
// " which corresponds to channel " + channelNumber + " and program id " +
// schedule.programID + " which says it has images? " +
// programDict[schedule.programID].hasImageArtwork);
if (images != null)
{
var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
if (imageIndex > -1)
{
var programEntry = programDict[schedule.programID];
var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
const double DesiredAspect = 2.0 / 3;
var programEntry = programDict[schedule.programID];
programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
const double WideAspect = 16.0 / 9;
const double DesiredAspect = 2.0 / 3;
programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
// Don't supply the same image twice
if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
{
programEntry.thumbImage = null;
}
const double WideAspect = 16.0 / 9;
programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
// GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
// GetProgramImage(ApiUrl, data, "Banner-LOT", false);
}
// Don't supply the same image twice
if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
{
programEntry.thumbImage = null;
}
programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
}
programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
return programsInfo;
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
// GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
// GetProgramImage(ApiUrl, data, "Banner-LOT", false);
}
}
programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
}
return programsInfo;
}
private static int GetSizeOrder(ScheduleDirect.ImageData image)
@ -367,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (!string.IsNullOrWhiteSpace(details.originalAirDate))
{
info.OriginalAirDate = DateTime.Parse(details.originalAirDate);
info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
info.ProductionYear = info.OriginalAirDate.Value.Year;
}
if (details.movie != null)
{
if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year))
if (!string.IsNullOrEmpty(details.movie.year)
&& int.TryParse(details.movie.year, out int year))
{
info.ProductionYear = year;
}
@ -482,22 +468,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
imageIdString = imageIdString.TrimEnd(',') + "]";
var httpOptions = new HttpRequestOptions()
using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
{
Url = ApiUrl + "/metadata/programs",
UserAgent = UserAgent,
CancellationToken = cancellationToken,
RequestContent = imageIdString,
LogErrorResponseBody = true,
Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json)
};
try
{
using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false))
{
return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
innerResponse2.Content).ConfigureAwait(false);
}
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
response).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -518,41 +499,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return lineups;
}
var options = new HttpRequestOptions()
{
Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location,
UserAgent = UserAgent,
CancellationToken = cancellationToken,
LogErrorResponseBody = true
};
options.RequestHeaders["token"] = token;
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location);
options.Headers.TryAddWithoutValidation("token", token);
try
{
using (var httpResponse = await Get(options, false, info).ConfigureAwait(false))
using (Stream responce = httpResponse.Content)
{
var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(responce).ConfigureAwait(false);
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
if (root != null)
if (root != null)
{
foreach (ScheduleDirect.Headends headend in root)
{
foreach (ScheduleDirect.Headends headend in root)
foreach (ScheduleDirect.Lineup lineup in headend.lineups)
{
foreach (ScheduleDirect.Lineup lineup in headend.lineups)
lineups.Add(new NameIdPair
{
lineups.Add(new NameIdPair
{
Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
Id = lineup.uri.Substring(18)
});
}
Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
Id = lineup.uri.Substring(18)
});
}
}
else
{
_logger.LogInformation("No lineups available");
}
}
else
{
_logger.LogInformation("No lineups available");
}
}
catch (Exception ex)
@ -587,7 +560,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null;
}
NameValuePair savedToken = null;
NameValuePair savedToken;
if (!_tokens.TryGetValue(username, out savedToken))
{
savedToken = new NameValuePair();
@ -633,16 +606,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
private async Task<HttpResponseInfo> Post(HttpRequestOptions options,
private async Task<HttpResponseMessage> Send(
HttpRequestMessage options,
bool enableRetry,
ListingsProviderInfo providerInfo)
ListingsProviderInfo providerInfo,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
// Schedules direct requires that the client support compression and will return a 400 response without it
options.DecompressionMethod = CompressionMethods.Deflate;
try
{
return await _httpClient.Post(options).ConfigureAwait(false);
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
}
catch (HttpException ex)
{
@ -659,65 +632,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
return await Post(options, false, providerInfo).ConfigureAwait(false);
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
}
private async Task<HttpResponseInfo> Get(HttpRequestOptions options,
bool enableRetry,
ListingsProviderInfo providerInfo)
{
// Schedules direct requires that the client support compression and will return a 400 response without it
options.DecompressionMethod = CompressionMethods.Deflate;
try
{
return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
}
catch (HttpException ex)
{
_tokens.Clear();
if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
{
enableRetry = false;
}
if (!enableRetry)
{
throw;
}
}
options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
return await Get(options, false, providerInfo).ConfigureAwait(false);
}
private async Task<string> GetTokenInternal(string username, string password,
private async Task<string> GetTokenInternal(
string username,
string password,
CancellationToken cancellationToken)
{
var httpOptions = new HttpRequestOptions()
{
Url = ApiUrl + "/token",
UserAgent = UserAgent,
RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}",
CancellationToken = cancellationToken,
LogErrorResponseBody = true
};
// _logger.LogInformation("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " +
// httpOptions.RequestContent);
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using (var response = await Post(httpOptions, false, null).ConfigureAwait(false))
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
if (root.message == "OK")
{
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(response.Content).ConfigureAwait(false);
if (root.message == "OK")
{
_logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
return root.token;
}
throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
_logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
return root.token;
}
throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
}
private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@ -736,20 +672,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_logger.LogInformation("Adding new LineUp ");
var httpOptions = new HttpRequestOptions()
{
Url = ApiUrl + "/lineups/" + info.ListingsId,
UserAgent = UserAgent,
CancellationToken = cancellationToken,
LogErrorResponseBody = true,
BufferContent = false
};
httpOptions.RequestHeaders["token"] = token;
using (await _httpClient.SendAsync(httpOptions, HttpMethod.Put).ConfigureAwait(false))
{
}
using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId);
options.Headers.TryAddWithoutValidation("token", token);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
}
private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
@ -768,25 +693,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_logger.LogInformation("Headends on account ");
var options = new HttpRequestOptions()
{
Url = ApiUrl + "/lineups",
UserAgent = UserAgent,
CancellationToken = cancellationToken,
LogErrorResponseBody = true
};
options.RequestHeaders["token"] = token;
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups");
options.Headers.TryAddWithoutValidation("token", token);
try
{
using (var httpResponse = await Get(options, false, null).ConfigureAwait(false))
using (var response = httpResponse.Content)
{
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(response).ConfigureAwait(false);
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var response = httpResponse.Content;
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
catch (HttpException ex)
{
@ -851,55 +768,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings
throw new Exception("token required");
}
var httpOptions = new HttpRequestOptions()
{
Url = ApiUrl + "/lineups/" + listingsId,
UserAgent = UserAgent,
CancellationToken = cancellationToken,
LogErrorResponseBody = true,
};
httpOptions.RequestHeaders["token"] = token;
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
options.Headers.TryAddWithoutValidation("token", token);
var list = new List<ChannelInfo>();
using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false))
using (var response = httpResponse.Content)
{
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(response).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
foreach (ScheduleDirect.Map map in root.map)
{
var channelNumber = GetChannelNumber(map);
var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
if (station == null)
{
station = new ScheduleDirect.Station
{
stationID = map.stationID
};
}
foreach (ScheduleDirect.Map map in root.map)
{
var channelNumber = GetChannelNumber(map);
var channelInfo = new ChannelInfo
{
Id = station.stationID,
CallSign = station.callsign,
Number = channelNumber,
Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
};
var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
if (station == null)
{
station = new ScheduleDirect.Station { stationID = map.stationID };
}
if (station.logo != null)
{
channelInfo.ImageUrl = station.logo.URL;
}
var channelInfo = new ChannelInfo
{
Id = station.stationID,
CallSign = station.callsign,
Number = channelNumber,
Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
};
list.Add(channelInfo);
if (station.logo != null)
{
channelInfo.ImageUrl = station.logo.URL;
}
list.Add(channelInfo);
}
return list;

@ -25,20 +25,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public class XmlTvListingsProvider : IListingsProvider
{
private readonly IServerConfigurationManager _config;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<XmlTvListingsProvider> _logger;
private readonly IFileSystem _fileSystem;
private readonly IZipClient _zipClient;
public XmlTvListingsProvider(
IServerConfigurationManager config,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
ILogger<XmlTvListingsProvider> logger,
IFileSystem fileSystem,
IZipClient zipClient)
{
_config = config;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_logger = logger;
_fileSystem = fileSystem;
_zipClient = zipClient;
@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
using (var res = await _httpClient.SendAsync(
new HttpRequestOptions
{
CancellationToken = cancellationToken,
Url = path,
DecompressionMethod = CompressionMethods.Gzip,
},
HttpMethod.Get).ConfigureAwait(false))
using (var stream = res.Content)
using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
{
if (res.ContentHeaders.ContentEncoding.Contains("gzip"))
{
using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
{
await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
}
else
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
return UnzipIfNeeded(path, cacheFile);

@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IServerConfigurationManager config,
ILogger<HdHomerunHost> logger,
IFileSystem fileSystem,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
INetworkManager networkManager,
@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
{
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_socketFactory = socketFactory;
_networkManager = networkManager;
@ -71,15 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
var options = new HttpRequestOptions
{
Url = model.LineupURL,
CancellationToken = cancellationToken,
BufferContent = false
};
using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
await using var stream = response.Content;
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
@ -133,14 +126,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try
{
using var response = await _httpClient.SendAsync(
new HttpRequestOptions
{
Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)),
CancellationToken = cancellationToken,
BufferContent = false
}, HttpMethod.Get).ConfigureAwait(false);
await using var stream = response.Content;
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
@ -183,48 +172,41 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using (var response = await _httpClient.SendAsync(
new HttpRequestOptions()
{
Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)),
CancellationToken = cancellationToken,
BufferContent = false
},
HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
{
var tuners = new List<LiveTvTunerInfo>();
while (!sr.EndOfStream)
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>();
while (!sr.EndOfStream)
{
string line = StripXML(sr.ReadLine());
if (line.Contains("Channel", StringComparison.Ordinal))
{
string line = StripXML(sr.ReadLine());
if (line.Contains("Channel", StringComparison.Ordinal))
LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = line.Substring(0, index - 1);
var currentChannel = line.Substring(index + 7);
if (currentChannel != "none")
{
LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = line.Substring(0, index - 1);
var currentChannel = line.Substring(index + 7);
if (currentChannel != "none")
{
status = LiveTvTunerStatus.LiveTv;
}
else
{
status = LiveTvTunerStatus.Available;
}
tuners.Add(new LiveTvTunerInfo
{
Name = name,
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
ProgramName = currentChannel,
Status = status
});
status = LiveTvTunerStatus.LiveTv;
}
else
{
status = LiveTvTunerStatus.Available;
}
}
return tuners;
tuners.Add(new LiveTvTunerInfo
{
Name = name,
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
ProgramName = currentChannel,
Status = status
});
}
}
return tuners;
}
private static string StripXML(string source)
@ -634,7 +616,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
info,
streamId,
FileSystem,
_httpClient,
_httpClientFactory,
Logger,
Config,
_appHost,

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
private readonly IMediaSourceManager _mediaSourceManager;
@ -37,14 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IMediaSourceManager mediaSourceManager,
ILogger<M3UTunerHost> logger,
IFileSystem fileSystem,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
INetworkManager networkManager,
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
{
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_networkManager = networkManager;
_mediaSourceManager = mediaSourceManager;
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@ -116,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config, _appHost, _streamHelper);
return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
}
@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
{
}
}

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@ -19,13 +20,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public class M3uParser
{
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost)
public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
{
_logger = logger;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
}
@ -51,13 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return _httpClient.Get(new HttpRequestOptions
{
Url = url,
CancellationToken = cancellationToken,
// Some data providers will require a user agent
UserAgent = _appHost.ApplicationUserAgent
});
return _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync(url);
}
return Task.FromResult((Stream)File.OpenRead(url));

@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class SharedHttpStream : LiveStream, IDirectStreamProvider
{
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
public SharedHttpStream(
@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
TunerHostInfo tunerHostInfo,
string originalStreamId,
IFileSystem fileSystem,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
ILogger logger,
IConfigurationManager configurationManager,
IServerApplicationHost appHost,
IStreamHelper streamHelper)
: base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
OriginalStreamId = originalStreamId;
EnableStreamSharing = true;
@ -55,25 +55,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
var httpRequestOptions = new HttpRequestOptions
{
Url = url,
CancellationToken = CancellationToken.None,
BufferContent = false,
DecompressionMethod = CompressionMethods.None
};
foreach (var header in mediaSource.RequiredHttpHeaders)
{
httpRequestOptions.RequestHeaders[header.Key] = header.Value;
}
var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
var extension = "ts";
var requiresRemux = false;
var contentType = response.ContentType ?? string.Empty;
var contentType = response.Content.Headers.ContentType.ToString();
if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
{
requiresRemux = true;
@ -132,24 +121,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>
{
try
{
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
using (response)
using (var stream = response.Content)
using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
await StreamHelper.CopyToAsync(
stream,
fileStream,
IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
using var message = response;
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
await StreamHelper.CopyToAsync(
stream,
fileStream,
IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{

@ -1,19 +1,19 @@
{
"Artists": "Kunstenare",
"Channels": "Kanale",
"Folders": "Fouers",
"Favorites": "Gunstelinge",
"Folders": "Lêergidse",
"Favorites": "Gunstellinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Rolprente",
"Shows": "Program",
"HeaderContinueWatching": "Hou Aan Kyk",
"Movies": "Flieks",
"Shows": "Televisie Reekse",
"HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Fotos",
"Playlists": "Speellysse",
"Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
@ -23,7 +23,7 @@
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
"HeaderLiveTV": "Live TV",
"HeaderLiveTV": "Lewendige TV",
"Application": "Program",
"AppDeviceValues": "App: {0}, Toestel: {1}",
"VersionNumber": "Weergawe {0}",
@ -95,5 +95,23 @@
"TasksChannelsCategory": "Internet kanale",
"TasksApplicationCategory": "aansoek",
"TasksLibraryCategory": "biblioteek",
"TasksMaintenanceCategory": "onderhoud"
"TasksMaintenanceCategory": "onderhoud",
"TaskCleanCacheDescription": "Vee kasregister lêers uit wat nie meer deur die stelsel benodig word nie.",
"TaskCleanCache": "Reinig Kasgeheue Lêergids",
"TaskDownloadMissingSubtitlesDescription": "Soek aanlyn vir vermiste onderskrifte gebasseer op metadata verstellings.",
"TaskDownloadMissingSubtitles": "Laai vermiste onderskrifte af",
"TaskRefreshChannelsDescription": "Vervris internet kanaal inligting.",
"TaskRefreshChannels": "Vervris Kanale",
"TaskCleanTranscodeDescription": "Vee transkodering lêers uit wat ouer is as een dag.",
"TaskCleanTranscode": "Reinig Transkoderings Leêrbinder",
"TaskUpdatePluginsDescription": "Laai opgedateerde inprop-sagteware af en installeer inprop-sagteware wat verstel is om outomaties op te dateer.",
"TaskUpdatePlugins": "Dateer Inprop-Sagteware Op",
"TaskRefreshPeopleDescription": "Vervris metadata oor akteurs en regisseurs in u media versameling.",
"TaskRefreshPeople": "Vervris Mense",
"TaskCleanLogsDescription": "Vee loglêers wat ouer as {0} dae is uit.",
"TaskCleanLogs": "Reinig Loglêer Lêervouer",
"TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
"TaskRefreshLibrary": "Skandeer Media Versameling",
"TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
"TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
}

@ -107,7 +107,7 @@
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshLibrary": "Scanner toute les Bibliothèques",
"TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
"TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

@ -1,3 +1,11 @@
{
"Albums": "Álbumes"
"Albums": "Álbumes",
"Collections": "Colecións",
"ChapterNameValue": "Capítulos {0}",
"Channels": "Canles",
"CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
"Books": "Libros",
"AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
"Artists": "Artistas",
"Application": "Aplicativo"
}

@ -19,7 +19,7 @@
"HeaderFavoriteEpisodes": "Episode Favorit",
"HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Lanjutkan Menonton",
"HeaderContinueWatching": "Lanjut Menonton",
"HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis",
"Genres": "Aliran",

@ -84,8 +84,8 @@
"UserDeletedWithName": "사용자 {0} 삭제됨",
"UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다",
"UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다",
"UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다",
"UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다",
"UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴",
"UserOnlineFromDevice": "{0}이 {1}으로 접속",
"UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다",
"UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다",
"UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중",

@ -45,12 +45,12 @@
"NameSeasonNumber": "Sesong {0}",
"NameSeasonUnknown": "Sesong ukjent",
"NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
"NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
"NotificationOptionAudioPlayback": "Lydavspilling startet",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
"NotificationOptionInstallationFailed": "Installasjonsfeil",
"NotificationOptionInstallationFailed": "Installasjonen feilet",
"NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
"NotificationOptionPluginError": "Pluginfeil",
"NotificationOptionPluginInstalled": "Plugin installert",
@ -71,7 +71,7 @@
"ScheduledTaskFailedWithName": "{0} mislykkes",
"ScheduledTaskStartedWithName": "{0} startet",
"ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
"Shows": "Programmer",
"Shows": "Program",
"Songs": "Sanger",
"StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
"SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}",
@ -88,7 +88,7 @@
"UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
"UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
"UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}",
"UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}",
"UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}",
"UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}",
"ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
"ValueSpecialEpisodeName": "Spesialepisode - {0}",

@ -35,7 +35,7 @@
"AuthenticationSucceededWithUserName": "{0} Har logga inn",
"Artists": "Artistar",
"Application": "Program",
"AppDeviceValues": "App: {0}, Einheit: {1}",
"AppDeviceValues": "App: {0}, Eining: {1}",
"Albums": "Album",
"NotificationOptionServerRestartRequired": "Tenaren krev omstart",
"NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
@ -43,7 +43,7 @@
"NotificationOptionPluginInstalled": "Tilleggsprogram installert",
"NotificationOptionPluginError": "Tilleggsprogram feila",
"NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
"NotificationOptionInstallationFailed": "Installasjonen feila",
"NotificationOptionInstallationFailed": "Installasjonsfeil",
"NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa",
"NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
@ -56,5 +56,62 @@
"MusicVideos": "Musikkvideoar",
"Music": "Musikk",
"Movies": "Filmar",
"MixedContent": "Blanda innhald"
"MixedContent": "Blanda innhald",
"Sync": "Synkronisera",
"TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
"TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
"TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
"TaskRefreshChannels": "Oppdater kanalar",
"TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.",
"TaskCleanTranscode": "Reins transkodemappe",
"TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.",
"TaskUpdatePlugins": "Oppdaterer programtillegg",
"TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.",
"TaskRefreshPeople": "Oppdater personar",
"TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.",
"TaskCleanLogs": "Reins loggmappe",
"TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.",
"TaskRefreshLibrary": "Skann mediebibliotek",
"TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.",
"TaskRefreshChapterImages": "Trekk ut kapittelbilete",
"TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.",
"TaskCleanCache": "Rens mappe for hurtiglager",
"TasksChannelsCategory": "Internettkanalar",
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedlikehald",
"VersionNumber": "Versjon {0}",
"ValueSpecialEpisodeName": "Spesialepisode - {0}",
"ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
"UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}",
"UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}",
"UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}",
"UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
"UserOnlineFromDevice": "{0} er direktekopla frå {1}",
"UserOfflineFromDevice": "{0} har kopla frå {1}",
"UserLockedOutWithName": "Brukar {0} har blitt utestengd",
"UserDownloadingItemWithValues": "{0} lastar ned {1}",
"UserDeletedWithName": "Brukar {0} er sletta",
"UserCreatedWithName": "Brukar {0} er oppretta",
"User": "Brukar",
"TvShows": "TV-seriar",
"System": "System",
"SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}",
"StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.",
"Songs": "Songar",
"Shows": "Program",
"ServerNameNeedsToBeRestarted": "{0} må omstartast",
"ScheduledTaskStartedWithName": "{0} starta",
"ScheduledTaskFailedWithName": "{0} feila",
"ProviderValue": "Leverandør: {0}",
"PluginUpdatedWithName": "{0} blei oppdatert",
"PluginUninstalledWithName": "{0} blei avinstallert",
"PluginInstalledWithName": "{0} blei installert",
"Plugin": "Programtillegg",
"Playlists": "Speleliste",
"Photos": "Foto",
"NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
"NotificationOptionVideoPlayback": "Videoavspeling starta",
"NotificationOptionUserLockedOut": "Brukar er utestengd",
"NotificationOptionTaskFailed": "Planlagt oppgåve feila"
}

@ -0,0 +1,117 @@
{
"MessageApplicationUpdatedTo": "Serveri Jellyfin u përditesua në versionin {0}",
"Inherit": "Trashgimi",
"TaskDownloadMissingSubtitlesDescription": "Kërkon në internet për titra që mungojnë bazuar tek konfigurimi i metadata-ve.",
"TaskDownloadMissingSubtitles": "Shkarko titra që mungojnë",
"TaskRefreshChannelsDescription": "Rifreskon informacionin e kanaleve të internetit.",
"TaskRefreshChannels": "Rifresko Kanalet",
"TaskCleanTranscodeDescription": "Fshin skedarët e transkodimit që janë më të vjetër se një ditë.",
"TaskCleanTranscode": "Fshi dosjen e transkodimit",
"TaskUpdatePluginsDescription": "Shkarkon dhe instalon përditësimi për plugin që janë konfiguruar të përditësohen automatikisht.",
"TaskUpdatePlugins": "Përditëso Plugin",
"TaskRefreshPeopleDescription": "Përditëson metadata të aktorëve dhe regjizorëve në librarinë tuaj.",
"TaskRefreshPeople": "Rifresko aktorët",
"TaskCleanLogsDescription": "Fshin skëdarët log që janë më të vjetër se {0} ditë.",
"TaskCleanLogs": "Fshi dosjen Log",
"TaskRefreshLibraryDescription": "Skanon librarinë media për skedarë të rinj dhe rifreskon metadata.",
"TaskRefreshLibrary": "Skano librarinë media",
"TaskRefreshChapterImagesDescription": "Krijon imazh për videot që kanë kapituj.",
"TaskRefreshChapterImages": "Ekstrakto Imazhet e Kapitullit",
"TaskCleanCacheDescription": "Fshi skedarët e cache-s që nuk i duhen më sistemit.",
"TaskCleanCache": "Pastro memorjen cache",
"TasksChannelsCategory": "Kanalet nga interneti",
"TasksApplicationCategory": "Aplikacioni",
"TasksLibraryCategory": "Libraria",
"TasksMaintenanceCategory": "Mirëmbajtje",
"VersionNumber": "Versioni {0}",
"ValueSpecialEpisodeName": "Speciale - {0}",
"ValueHasBeenAddedToLibrary": "{0} u shtua tek libraria juaj",
"UserStoppedPlayingItemWithValues": "{0} mbaroi së shikuari {1} tek {2}",
"UserStartedPlayingItemWithValues": "{0} po shikon {1} tek {2}",
"UserPolicyUpdatedWithName": "Politika e përdoruesit u përditësua për {0}",
"UserPasswordChangedWithName": "Fjalëkalimi u ndryshua për përdoruesin {0}",
"UserOnlineFromDevice": "{0} është në linjë nga {1}",
"UserOfflineFromDevice": "{0} u shkëput nga {1}",
"UserLockedOutWithName": "Përdoruesi {0} u përjashtua",
"UserDownloadingItemWithValues": "{0} po shkarkon {1}",
"UserDeletedWithName": "Përdoruesi {0} u fshi",
"UserCreatedWithName": "Përdoruesi {0} u krijua",
"User": "Përdoruesi",
"TvShows": "Seriale TV",
"System": "Sistemi",
"Sync": "Sinkronizo",
"SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}",
"StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.",
"Songs": "Këngë",
"Shows": "Seriale",
"ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj",
"ScheduledTaskStartedWithName": "{0} filloi",
"ScheduledTaskFailedWithName": "{0} dështoi",
"ProviderValue": "Ofruesi: {0}",
"PluginUpdatedWithName": "{0} u përditësua",
"PluginUninstalledWithName": "{0} u çinstalua",
"PluginInstalledWithName": "{0} u instalua",
"Plugin": "Plugin",
"Playlists": "Listat për luajtje",
"Photos": "Fotografitë",
"NotificationOptionVideoPlaybackStopped": "Luajtja e videos ndaloi",
"NotificationOptionVideoPlayback": "Luajtja e videos filloi",
"NotificationOptionUserLockedOut": "Përdoruesi u përjashtua",
"NotificationOptionTaskFailed": "Ushtrimi i planifikuar dështoi",
"NotificationOptionServerRestartRequired": "Kërkohet ristartim i serverit",
"NotificationOptionPluginUpdateInstalled": "Përditësimi i plugin u instalua",
"NotificationOptionPluginUninstalled": "Plugin u çinstalua",
"NotificationOptionPluginInstalled": "Plugin u instalua",
"NotificationOptionPluginError": "Plugin dështoi",
"NotificationOptionNewLibraryContent": "Një përmbajtje e re u shtua",
"NotificationOptionInstallationFailed": "Instalimi dështoi",
"NotificationOptionCameraImageUploaded": "Fotoja nga kamera u ngarkua",
"NotificationOptionAudioPlaybackStopped": "Luajtja e audios ndaloi",
"NotificationOptionAudioPlayback": "Luajtja e audios filloi",
"NotificationOptionApplicationUpdateInstalled": "Përditësimi i aplikacionit u instalua",
"NotificationOptionApplicationUpdateAvailable": "Një perditësim i aplikacionit është gati",
"NewVersionIsAvailable": "Një version i ri i Jellyfin është gati për tu shkarkuar.",
"NameSeasonUnknown": "Sezon i panjohur",
"NameSeasonNumber": "Sezoni {0}",
"NameInstallFailed": "Instalimi i {0} dështoi",
"MusicVideos": "Video muzikore",
"Music": "Muzikë",
"Movies": "Filma",
"MixedContent": "Përmbajtje e përzier",
"MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan",
"MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua",
"MessageApplicationUpdated": "Serveri Jellyfin u përditësua",
"Latest": "Të fundit",
"LabelRunningTimeValue": "Kohëzgjatja: {0}",
"LabelIpAddressValue": "Adresa IP: {0}",
"ItemRemovedWithName": "{0} u fshi nga libraria",
"ItemAddedWithName": "{0} u shtua tek libraria",
"HomeVideos": "Video personale",
"HeaderRecordingGroups": "Grupet e regjistrimit",
"HeaderNextUp": "Në vazhdim",
"HeaderLiveTV": "TV Live",
"HeaderFavoriteSongs": "Kënget e preferuara",
"HeaderFavoriteShows": "Serialet e preferuar",
"HeaderFavoriteEpisodes": "Episodet e preferuar",
"HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
"HeaderCameraUploads": "Ngarkimet nga Kamera",
"HeaderAlbumArtists": "Artistët e albumeve",
"Genres": "Zhanre",
"Folders": "Dosje",
"Favorites": "Të preferuara",
"FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
"DeviceOnlineWithName": "{0} u lidh",
"DeviceOfflineWithName": "{0} u shkëput",
"Collections": "Koleksione",
"ChapterNameValue": "Kapituj",
"Channels": "Kanale",
"CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
"Books": "Libra",
"AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
"Artists": "Artistë",
"Application": "Aplikacioni",
"AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
"Albums": "Albumet"
}

@ -18,7 +18,7 @@
"MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
"MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
"MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
"Inherit": "மரபரிமையாகப் பெறு",
"Inherit": "மரபரிமையாகப் பெறு",
"HeaderRecordingGroups": "பதிவு குழுக்கள்",
"HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
"Folders": "கோப்புறைகள்",
@ -26,12 +26,12 @@
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
"Collections": "தொகுப்புகள்",
"CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
"CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
"AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
"TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
"TaskRefreshChannels": "சேனல்களை புதுப்பி",
"TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
"TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்",
"TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்",
"TasksChannelsCategory": "இணைய சேனல்கள்",
"TasksApplicationCategory": "செயலி",
"TasksLibraryCategory": "நூலகம்",
@ -46,7 +46,7 @@
"Sync": "ஒத்திசைவு",
"StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
"Songs": "பாடல்கள்",
"Shows": "தொடர்கள்",
"Shows": "நிகழ்ச்சிகள்",
"ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
"ScheduledTaskStartedWithName": "{0} துவங்கியது",
"ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
@ -67,20 +67,20 @@
"NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
"NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
"NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
"NameSeasonUnknown": "பருவம் அறியப்படாதவை",
"NameSeasonUnknown": "அறியப்படாத பருவம்",
"NameSeasonNumber": "பருவம் {0}",
"NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
"MusicVideos": "இசைப்படங்கள்",
"Music": "இசை",
"Movies": "திரைப்படங்கள்",
"Latest": "புதிய",
"Latest": "புதியவை",
"LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
"LabelIpAddressValue": "ஐபி முகவரி: {0}",
"ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
"ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
"HeaderNextUp": "அடுத்ததாக",
"HeaderNextUp": "அடுத்தத",
"HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
"HeaderFavoriteSongs": "பிடித்த பாடடுகள்",
"HeaderFavoriteSongs": "பிடித்த பாட்கள்",
"HeaderFavoriteShows": "பிடித்த தொடர்கள்",
"HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
"HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
@ -93,25 +93,25 @@
"Channels": "சேனல்கள்",
"Books": "புத்தகங்கள்",
"AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
"Artists": "கலைஞர்",
"Artists": "கலைஞர்கள்",
"Application": "செயலி",
"Albums": "ஆல்பங்கள்",
"NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
"MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
"MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது",
"TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
"TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
"TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
"TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.",
"TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
"ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
"UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
"HomeVideos": "முகப்பு வீடியோக்கள்",
"UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
"UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
"UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
}

@ -1,76 +1,117 @@
{
"ProviderValue": "ผู้ให้บริการ: {0}",
"PluginUpdatedWithName": "{0} ได้รับการ update แล้ว",
"PluginUninstalledWithName": "ถอนการติดตั้ง {0}",
"PluginInstalledWithName": "{0} ได้รับการติดตั้ง",
"Plugin": "Plugin",
"Playlists": "รายการ",
"PluginUpdatedWithName": "อัปเดต {0} แล้ว",
"PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว",
"PluginInstalledWithName": "ติดตั้ง {0} แล้ว",
"Plugin": "ปลั๊กอิน",
"Playlists": "เพลย์ลิสต์",
"Photos": "รูปภาพ",
"NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video",
"NotificationOptionVideoPlayback": "เริ่มแสดง Video",
"NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out",
"NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว",
"NotificationOptionServerRestartRequired": "ควร Restart Server",
"NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว",
"NotificationOptionPluginUninstalled": "ถอด Plugin",
"NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว",
"NotificationOptionPluginError": "Plugin ล้มเหลว",
"NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว",
"NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว",
"NotificationOptionCameraImageUploaded": "รูปภาพถูก upload",
"NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง",
"NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ",
"NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ",
"NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก",
"NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว",
"NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์",
"NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว",
"NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว",
"NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว",
"NotificationOptionPluginError": "ปลั๊กอินล้มเหลว",
"NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว",
"NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว",
"NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
"NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
"NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
"NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว",
"NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว",
"NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่",
"NameSeasonUnknown": "ไม่ทราบปี",
"NameSeasonNumber": "ปี {0}",
"NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ",
"MusicVideos": "MV",
"Music": "เพลง",
"Movies": "ภาพยนต์",
"MixedContent": "รายการแบบผสม",
"MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว",
"MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว",
"MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}",
"MessageApplicationUpdated": "Jellyfin Server update แล้ว",
"NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอปพลิเคชันแล้ว",
"NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอปพลิเคชัน",
"NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
"NameSeasonUnknown": "ไม่ทราบซีซัน",
"NameSeasonNumber": "ซีซัน {0}",
"NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
"MusicVideos": "มิวสิควิดีโอ",
"Music": "ดนตรี",
"Movies": "ภาพยนต์",
"MixedContent": "เนื้อหาผสม",
"MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว",
"MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว",
"MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}",
"MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว",
"Latest": "ล่าสุด",
"LabelRunningTimeValue": "เวลาที่เล่น : {0}",
"LabelIpAddressValue": "IP address: {0}",
"ItemRemovedWithName": "{0} ถูกลบจากรายการ",
"ItemAddedWithName": "{0} ถูกเพิ่มในรายการ",
"Inherit": "การสืบทอด",
"HomeVideos": "วีดีโอส่วนตัว",
"HeaderRecordingGroups": "ค่ายบันทึก",
"LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
"LabelIpAddressValue": "ที่อยู่ IP: {0}",
"ItemRemovedWithName": "{0} ถูกลบออกจากไลบราร",
"ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว",
"Inherit": "สืบทอด",
"HomeVideos": "โฮมวิดีโอ",
"HeaderRecordingGroups": "กลุ่มการบันทึก",
"HeaderNextUp": "ถัดไป",
"HeaderLiveTV": "รายการสด",
"HeaderFavoriteSongs": "เพลงโปรด",
"HeaderFavoriteShows": "รายการโชว์โปรด",
"HeaderFavoriteEpisodes": "ฉากโปรด",
"HeaderFavoriteArtists": "นักแสดงโปรด",
"HeaderFavoriteAlbums": "อัมบั้มโปรด",
"HeaderContinueWatching": "ชมต่อจากเดิม",
"HeaderCameraUploads": "Upload รูปภาพ",
"HeaderAlbumArtists": "อัลบั้มักแสดง",
"HeaderLiveTV": "ทีวีสด",
"HeaderFavoriteSongs": "เพลงที่ชื่นชอบ",
"HeaderFavoriteShows": "รายการที่ชื่นชอบ",
"HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
"HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
"HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ",
"HeaderCameraUploads": "อัปโหลดรูปถ่าย",
"HeaderAlbumArtists": "อัลบั้มศิลปิน",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด",
"FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}",
"DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ",
"DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ",
"Collections": "ชุด",
"ChapterNameValue": "บทที่ {0}",
"Channels": "ชาแนล",
"CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}",
"FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}",
"DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
"DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
"Collections": "คอลเลกชัน",
"ChapterNameValue": "บท {0}",
"Channels": "ช่อง",
"CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
"Books": "หนังสือ",
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ",
"Artists": "ักแสดง",
"Application": "แอปพลิเคชัน",
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
"Artists": "ศิลปิน",
"Application": "แอปพลิเคชัน",
"AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}",
"Albums": "อัลบั้ม",
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
"ScheduledTaskFailedWithName": "{0} ล้มเหลว",
"Songs": "เพลง",
"Shows": "แสดง",
"ServerNameNeedsToBeRestarted": "{0} ต้องการรีสตาร์ท"
"Shows": "รายการ",
"ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
"TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
"TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
"TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
"TaskRefreshChannels": "รีเฟรชช่อง",
"TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน",
"TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด",
"TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ",
"TaskUpdatePlugins": "อัปเดตปลั๊กอิน",
"TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ",
"TaskRefreshPeople": "รีเฟรชบุคคล",
"TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน",
"TaskCleanLogs": "ล้างไดเรกทอรีบันทึก",
"TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา",
"TaskRefreshLibrary": "สแกนไลบรารีสื่อ",
"TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท",
"TaskRefreshChapterImages": "แตกรูปภาพบท",
"TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
"TaskCleanCache": "ล้างไดเรกทอรีแคช",
"TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
"TasksApplicationCategory": "แอปพลิเคชัน",
"TasksLibraryCategory": "ไลบรารี",
"TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
"VersionNumber": "เวอร์ชัน {0}",
"ValueSpecialEpisodeName": "พิเศษ - {0}",
"ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว",
"UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}",
"UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}",
"UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}",
"UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}",
"UserOnlineFromDevice": "{0} ออนไลน์จาก {1}",
"UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}",
"UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก",
"UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
"UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
"UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
"User": "ผู้ใช้งาน",
"TvShows": "รายการทีวี",
"System": "ระบบ",
"Sync": "ซิงค์",
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
"StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
}

@ -0,0 +1,117 @@
{
"Collections": "Bộ Sưu Tập",
"Favorites": "Yêu Thích",
"Folders": "Thư Mục",
"Genres": "Thể Loại",
"HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
"HeaderContinueWatching": "Xem Tiếp",
"HeaderLiveTV": "TV Trực Tiếp",
"Movies": "Phim",
"Photos": "Ảnh",
"Playlists": "Danh sách phát",
"Shows": "Chương Trình TV",
"Songs": "Các Bài Hát",
"Sync": "Đồng Bộ",
"ValueSpecialEpisodeName": "Đặc Biệt - {0}",
"Albums": "Albums",
"Artists": "Các Nghệ Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu",
"TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
"TaskRefreshChannels": "Làm Mới Kênh",
"TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
"TaskCleanTranscode": "Làm Sạch Thư Mục Chuyển Mã",
"TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
"TaskUpdatePlugins": "Cập Nhật Plugins",
"TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
"TaskRefreshPeople": "Làm mới Người dùng",
"TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
"TaskCleanLogs": "Làm sạch nhật ký",
"TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
"TaskRefreshLibrary": "Quét Thư viện Phương tiện",
"TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
"TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
"TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
"TaskCleanCache": "Làm Sạch Thư Mục Cache",
"TasksChannelsCategory": "Kênh Internet",
"TasksApplicationCategory": "Ứng Dụng",
"TasksLibraryCategory": "Thư Viện",
"TasksMaintenanceCategory": "Bảo Trì",
"VersionNumber": "Phiên Bản {0}",
"ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
"UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
"UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
"UserOnlineFromDevice": "{0} trực tuyến từ {1}",
"UserOfflineFromDevice": "{0} đã ngắt kết nối từ {1}",
"UserLockedOutWithName": "User {0} đã bị khóa",
"UserDownloadingItemWithValues": "{0} đang tải xuống {1}",
"UserDeletedWithName": "Người Dùng {0} đã được xóa",
"UserCreatedWithName": "Người Dùng {0} đã được tạo",
"User": "Người Dùng",
"TvShows": "Chương Trình TV",
"System": "Hệ Thống",
"SubtitleDownloadFailureFromForItem": "Không thể tải xuống phụ đề từ {0} cho {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server đang tải. Vui lòng thử lại trong thời gian ngắn.",
"ServerNameNeedsToBeRestarted": "{0} cần được khởi động lại",
"ScheduledTaskStartedWithName": "{0} đã bắt đầu",
"ScheduledTaskFailedWithName": "{0} đã thất bại",
"ProviderValue": "Provider: {0}",
"PluginUpdatedWithName": "{0} đã cập nhật",
"PluginUninstalledWithName": "{0} đã được gỡ bỏ",
"PluginInstalledWithName": "{0} đã được cài đặt",
"Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng",
"NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
"NotificationOptionUserLockedOut": "Người dùng bị khóa",
"NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
"NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
"NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
"NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
"NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
"NotificationOptionPluginError": "Thất bại Plugin",
"NotificationOptionNewLibraryContent": "Nội dung mới được thêm vào",
"NotificationOptionInstallationFailed": "Cài đặt thất bại",
"NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
"NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
"NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
"NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
"NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
"NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
"NameSeasonUnknown": "Không Rõ Mùa",
"NameSeasonNumber": "Mùa {0}",
"NameInstallFailed": "{0} cài đặt thất bại",
"MusicVideos": "Video Nhạc",
"Music": "Nhạc",
"MixedContent": "Nội dung hỗn hợp",
"MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",
"MessageNamedServerConfigurationUpdatedWithValue": "Phần cấu hình máy chủ {0} đã được cập nhật",
"MessageApplicationUpdatedTo": "Jellyfin Server đã được cập nhật lên {0}",
"MessageApplicationUpdated": "Jellyfin Server đã được cập nhật",
"Latest": "Gần Nhất",
"LabelRunningTimeValue": "Thời Gian Chạy: {0}",
"LabelIpAddressValue": "Địa Chỉ IP: {0}",
"ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
"ItemAddedWithName": "{0} được thêm vào thư viện",
"Inherit": "Thừa hưởng",
"HomeVideos": "Video nhà",
"HeaderRecordingGroups": "Nhóm Ghi Video",
"HeaderNextUp": "Tiếp Theo",
"HeaderFavoriteSongs": "Bài Hát Yêu Thích",
"HeaderFavoriteShows": "Chương Trình Yêu Thích",
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
"HeaderFavoriteAlbums": "Album Ưa Thích",
"HeaderCameraUploads": "Máy Ảnh Tải Lên",
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
"DeviceOnlineWithName": "{0} đã kết nối",
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
"ChapterNameValue": "Phân Cảnh {0}",
"Channels": "Các Kênh",
"CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
"Books": "Sách",
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
"Application": "Ứng Dụng",
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
}

@ -1,6 +1,6 @@
{
"Albums": "專輯",
"AppDeviceValues": "軟體: {0}, 裝置: {1}",
"AppDeviceValues": "軟體{0},裝置:{1}",
"Application": "應用程式",
"Artists": "演出者",
"AuthenticationSucceededWithUserName": "{0} 成功授權",
@ -11,7 +11,7 @@
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷線",
"DeviceOnlineWithName": "{0} 已經連線",
"FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
"FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
@ -28,8 +28,8 @@
"HomeVideos": "自製影片",
"ItemAddedWithName": "{0} 已新增至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
"LabelIpAddressValue": "IP 位置: {0}",
"LabelRunningTimeValue": "運行時間: {0}",
"LabelIpAddressValue": "IP 位址:{0}",
"LabelRunningTimeValue": "運行時間{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server 已經更新",
"MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
@ -42,18 +42,18 @@
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
"NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
"NewVersionIsAvailable": "新版本的 Jellyfin Server 軟體已經可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
"NotificationOptionApplicationUpdateInstalled": "軟體更新已安裝",
"NotificationOptionAudioPlayback": "音樂開始播放",
"NotificationOptionAudioPlaybackStopped": "音樂停止播放",
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
"NotificationOptionPluginError": "插件安裝錯誤",
"NotificationOptionPluginInstalled": "插件已安裝",
"NotificationOptionPluginUninstalled": "插件已移除",
"NotificationOptionPluginUpdateInstalled": "插件已更新",
"NotificationOptionPluginError": "外掛安裝失敗",
"NotificationOptionPluginInstalled": "外掛已安裝",
"NotificationOptionPluginUninstalled": "外掛已移除",
"NotificationOptionPluginUpdateInstalled": "外掛已更新",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
@ -61,14 +61,14 @@
"NotificationOptionVideoPlaybackStopped": "影片停止播放",
"Photos": "相片",
"Playlists": "播放清單",
"Plugin": "插件",
"Plugin": "外掛",
"PluginInstalledWithName": "{0} 已安裝",
"PluginUninstalledWithName": "{0} 已移除",
"PluginUpdatedWithName": "{0} 已更新",
"ProviderValue": "提供商: {0}",
"ScheduledTaskFailedWithName": "{0} 已失敗",
"ScheduledTaskStartedWithName": "{0} 已開始",
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
"ScheduledTaskFailedWithName": "排程任務 {0} 已失敗",
"ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
"ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin Server正在啟動請稍後再試一次。",
@ -78,10 +78,10 @@
"User": "使用者",
"UserCreatedWithName": "使用者 {0} 已建立",
"UserDeletedWithName": "使用者 {0} 已移除",
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
"UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
"UserLockedOutWithName": "使用者 {0} 已鎖定",
"UserOfflineFromDevice": "{0} 已從 {1} 斷線",
"UserOnlineFromDevice": "{0} 已連線,來自 {1}",
"UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
"UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
@ -95,23 +95,23 @@
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新插件",
"TaskRefreshPeople": "重新整理人員",
"TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。",
"TaskUpdatePlugins": "更新外掛",
"TaskRefreshPeople": "刷新用戶",
"TaskCleanLogsDescription": "刪除超過 {0} 天的紀錄檔。",
"TaskCleanLogs": "清空紀錄資料夾",
"TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。",
"TaskRefreshLibrary": "掃描媒體庫",
"TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。",
"TaskRefreshLibrary": "重新掃描媒體庫",
"TaskRefreshChapterImages": "擷取章節圖片",
"TaskCleanCacheDescription": "刪除系統長時間不需要的快取。",
"TaskCleanCacheDescription": "刪除系統不需要的快取。",
"TaskCleanCache": "清除快取資料夾",
"TasksLibraryCategory": "媒體庫",
"TaskRefreshChannelsDescription": "重新整理網頻道資料。",
"TaskRefreshChannelsDescription": "重新整理網頻道資料。",
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
"TaskCleanTranscode": "清除轉碼資料夾",
"TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
"TaskUpdatePluginsDescription": "為設置自動更新的外掛下載並安裝更新。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
"TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。",
"TasksChannelsCategory": "網頻道",
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
"TasksChannelsCategory": "網頻道",
"TasksApplicationCategory": "應用程式",
"TasksMaintenanceCategory": "維修"
}

@ -413,6 +413,7 @@ namespace Emby.Server.Implementations.Localization
yield return new LocalizationOption("Swedish", "sv");
yield return new LocalizationOption("Swiss German", "gsw");
yield return new LocalizationOption("Turkish", "tr");
yield return new LocalizationOption("Tiếng Việt", "vi");
}
}
}

@ -0,0 +1,60 @@
using System;
namespace Emby.Server.Implementations.Plugins
{
/// <summary>
/// Defines a Plugin manifest file.
/// </summary>
public class PluginManifest
{
/// <summary>
/// Gets or sets the category of the plugin.
/// </summary>
public string Category { get; set; }
/// <summary>
/// Gets or sets the changelog information.
/// </summary>
public string Changelog { get; set; }
/// <summary>
/// Gets or sets the description of the plugin.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the Global Unique Identifier for the plugin.
/// </summary>
public Guid Guid { get; set; }
/// <summary>
/// Gets or sets the Name of the plugin.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets an overview of the plugin.
/// </summary>
public string Overview { get; set; }
/// <summary>
/// Gets or sets the owner of the plugin.
/// </summary>
public string Owner { get; set; }
/// <summary>
/// Gets or sets the compatibility version for the plugin.
/// </summary>
public string TargetAbi { get; set; }
/// <summary>
/// Gets or sets the timestamp of the plugin.
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// Gets or sets the Version number of the plugin.
/// </summary>
public string Version { get; set; }
}
}

@ -5,10 +5,10 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
private IApplicationPaths ApplicationPaths { get; set; }
private readonly IApplicationPaths _applicationPaths;
private readonly ILogger<DeleteCacheFileTask> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
IFileSystem fileSystem,
ILocalizationManager localization)
{
ApplicationPaths = appPaths;
_applicationPaths = appPaths;
_logger = logger;
_fileSystem = fileSystem;
_localization = localization;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanCache");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
/// <inheritdoc />
public string Key => "DeleteCacheFiles";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[] {
return new[]
{
// Every so often
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
};
@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress);
}
catch (DirectoryNotFoundException)
{
@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress);
}
catch (DirectoryNotFoundException)
{
@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Task.CompletedTask;
}
/// <summary>
/// Deletes the cache files from directory with a last write time less than a given date.
/// </summary>
@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
_logger.LogError(ex, "Error deleting file {path}", path);
}
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanCache");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
/// <inheritdoc />
public string Key => "DeleteCacheFiles";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
}
}

@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
/// <inheritdoc />
public string Key => "PluginUpdates";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
progress.Report(100);
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
/// <inheritdoc />
public string Key => "PluginUpdates";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
}
}

@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class DailyTrigger : ITaskTrigger
{
/// <summary>
/// Get the time of day to trigger the task to run.
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
/// <summary>
/// Gets or sets the time of day to trigger the task to run.
/// </summary>
/// <value>The time of day.</value>
public TimeSpan TimeOfDay { get; set; }
@ -69,11 +74,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
}
/// <summary>
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
/// <summary>
/// Called when [triggered].
/// </summary>

@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class IntervalTrigger : ITaskTrigger
{
private DateTime _lastStartDate;
/// <summary>
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
/// <summary>
/// Gets or sets the interval.
/// </summary>
@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <value>The timer.</value>
private Timer Timer { get; set; }
private DateTime _lastStartDate;
/// <summary>
/// Stars waiting for the trigger action.
/// </summary>
@ -88,11 +93,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
}
/// <summary>
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
/// <summary>
/// Called when [triggered].
/// </summary>

@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class StartupTrigger : ITaskTrigger
{
/// <summary>
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
public int DelayMs { get; set; }
/// <summary>
@ -48,20 +53,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
}
/// <summary>
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
/// <summary>
/// Called when [triggered].
/// </summary>
private void OnTriggered()
{
if (Triggered != null)
{
Triggered(this, EventArgs.Empty);
}
Triggered?.Invoke(this, EventArgs.Empty);
}
}
}

@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class WeeklyTrigger : ITaskTrigger
{
/// <summary>
/// Get the time of day to trigger the task to run.
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
/// <summary>
/// Gets or sets the time of day to trigger the task to run.
/// </summary>
/// <value>The time of day.</value>
public TimeSpan TimeOfDay { get; set; }
@ -95,20 +100,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
}
/// <summary>
/// Occurs when [triggered].
/// </summary>
public event EventHandler<EventArgs> Triggered;
/// <summary>
/// Called when [triggered].
/// </summary>
private void OnTriggered()
{
if (Triggered != null)
{
Triggered(this, EventArgs.Empty);
}
Triggered?.Invoke(this, EventArgs.Empty);
}
}
}

@ -257,8 +257,7 @@ namespace Emby.Server.Implementations.Security
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts)
.ToList();
var statements = PrepareAll(db, statementTexts);
using (var statement = statements[0])
{
@ -282,7 +281,7 @@ namespace Emby.Server.Implementations.Security
ReadTransactionMode);
}
result.Items = list.ToArray();
result.Items = list;
return result;
}

@ -1,64 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
namespace Emby.Server.Implementations.Services
{
public class HttpResult
: IHttpResult, IAsyncStreamWriter
{
public HttpResult(object response, string contentType, HttpStatusCode statusCode)
{
this.Headers = new Dictionary<string, string>();
this.Response = response;
this.ContentType = contentType;
this.StatusCode = statusCode;
}
public object Response { get; set; }
public string ContentType { get; set; }
public IDictionary<string, string> Headers { get; private set; }
public int Status { get; set; }
public HttpStatusCode StatusCode
{
get => (HttpStatusCode)Status;
set => Status = (int)value;
}
public IRequest RequestContext { get; set; }
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{
var response = RequestContext?.Response;
if (this.Response is byte[] bytesResponse)
{
var contentLength = bytesResponse.Length;
if (response != null)
{
response.ContentLength = contentLength;
}
if (contentLength > 0)
{
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
}
return;
}
await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
}
}
}

@ -1,51 +0,0 @@
#pragma warning disable CS1591
using System;
using System.IO;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
namespace Emby.Server.Implementations.Services
{
public class RequestHelper
{
public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType)
{
switch (GetContentTypeWithoutEncoding(contentType))
{
case "application/xml":
case "text/xml":
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
return host.DeserializeXml;
case "application/json":
case "text/json":
return host.DeserializeJson;
}
return null;
}
public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType)
{
switch (GetContentTypeWithoutEncoding(contentType))
{
case "application/xml":
case "text/xml":
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
return host.SerializeToXml;
case "application/json":
case "text/json":
return host.SerializeToJson;
}
return null;
}
private static string GetContentTypeWithoutEncoding(string contentType)
{
return contentType?.Split(';')[0].ToLowerInvariant().Trim();
}
}
}

@ -1,141 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.Services
{
public static class ResponseHelper
{
public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken)
{
if (result == null)
{
if (response.StatusCode == (int)HttpStatusCode.OK)
{
response.StatusCode = (int)HttpStatusCode.NoContent;
}
response.ContentLength = 0;
return Task.CompletedTask;
}
var httpResult = result as IHttpResult;
if (httpResult != null)
{
httpResult.RequestContext = request;
request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType;
}
var defaultContentType = request.ResponseContentType;
if (httpResult != null)
{
if (httpResult.RequestContext == null)
{
httpResult.RequestContext = request;
}
response.StatusCode = httpResult.Status;
}
if (result is IHasHeaders responseOptions)
{
foreach (var responseHeaders in responseOptions.Headers)
{
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{
response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
continue;
}
response.Headers.Add(responseHeaders.Key, responseHeaders.Value);
}
}
// ContentType='text/html' is the default for a HttpResponse
// Do not override if another has been set
if (response.ContentType == null || response.ContentType == "text/html")
{
response.ContentType = defaultContentType;
}
if (response.ContentType == "application/json")
{
response.ContentType += "; charset=utf-8";
}
switch (result)
{
case IAsyncStreamWriter asyncStreamWriter:
return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken);
case IStreamWriter streamWriter:
streamWriter.WriteTo(response.Body);
return Task.CompletedTask;
case FileWriter fileWriter:
return fileWriter.WriteToAsync(response, cancellationToken);
case Stream stream:
return CopyStream(stream, response.Body);
case byte[] bytes:
response.ContentType = "application/octet-stream";
response.ContentLength = bytes.Length;
if (bytes.Length > 0)
{
return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
case string responseText:
var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
response.ContentLength = responseTextAsBytes.Length;
if (responseTextAsBytes.Length > 0)
{
return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
}
return Task.CompletedTask;
}
return WriteObject(request, result, response);
}
private static async Task CopyStream(Stream src, Stream dest)
{
using (src)
{
await src.CopyToAsync(dest).ConfigureAwait(false);
}
}
public static async Task WriteObject(IRequest request, object result, HttpResponse response)
{
var contentType = request.ResponseContentType;
var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
using (var ms = new MemoryStream())
{
serializer(result, ms);
ms.Position = 0;
var contentLength = ms.Length;
response.ContentLength = contentLength;
if (contentLength > 0)
{
await ms.CopyToAsync(response.Body).ConfigureAwait(false);
}
}
}
}
}

@ -1,202 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Services
{
public delegate object ActionInvokerFn(object intance, object request);
public delegate void VoidActionInvokerFn(object intance, object request);
public class ServiceController
{
private readonly ILogger<ServiceController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ServiceController"/> class.
/// </summary>
/// <param name="logger">The <see cref="ServiceController"/> logger.</param>
public ServiceController(ILogger<ServiceController> logger)
{
_logger = logger;
}
public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
{
foreach (var serviceType in serviceTypes)
{
RegisterService(appHost, serviceType);
}
}
public void RegisterService(HttpListenerHost appHost, Type serviceType)
{
// Make sure the provided type implements IService
if (!typeof(IService).IsAssignableFrom(serviceType))
{
_logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType);
return;
}
var processedReqs = new HashSet<Type>();
var actions = ServiceExecGeneral.Reset(serviceType);
foreach (var mi in serviceType.GetActions())
{
var requestType = mi.GetParameters()[0].ParameterType;
if (processedReqs.Contains(requestType))
{
continue;
}
processedReqs.Add(requestType);
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
// var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
// var responseType = returnMarker != null ?
// GetGenericArguments(returnMarker)[0]
// : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
// mi.ReturnType
// : Type.GetType(requestType.FullName + "Response");
RegisterRestPaths(appHost, requestType, serviceType);
appHost.AddServiceInfo(serviceType, requestType);
}
}
public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
{
var attrs = appHost.GetRouteAttributes(requestType);
foreach (var attr in attrs)
{
var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description);
RegisterRestPath(restPath);
}
}
private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
public void RegisterRestPath(RestPath restPath)
{
if (restPath.Path[0] != '/')
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"Route '{0}' on '{1}' must start with a '/'",
restPath.Path,
restPath.RequestType.GetMethodName()));
}
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"Route '{0}' on '{1}' contains invalid chars. ",
restPath.Path,
restPath.RequestType.GetMethodName()));
}
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
{
pathsAtFirstMatch.Add(restPath);
}
else
{
RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
}
}
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
{
var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
List<RestPath> firstMatches;
var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
foreach (var potentialHashMatch in yieldedHashMatches)
{
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
{
continue;
}
var bestScore = -1;
RestPath bestMatch = null;
foreach (var restPath in firstMatches)
{
var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
if (score > bestScore)
{
bestScore = score;
bestMatch = restPath;
}
}
if (bestScore > 0 && bestMatch != null)
{
return bestMatch;
}
}
var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
foreach (var potentialHashMatch in yieldedWildcardMatches)
{
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
{
continue;
}
var bestScore = -1;
RestPath bestMatch = null;
foreach (var restPath in firstMatches)
{
var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
if (score > bestScore)
{
bestScore = score;
bestMatch = restPath;
}
}
if (bestScore > 0 && bestMatch != null)
{
return bestMatch;
}
}
return null;
}
public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
{
var requestType = requestDto.GetType();
req.OperationName = requestType.Name;
var serviceType = httpHost.GetServiceTypeByRequest(requestType);
var service = httpHost.CreateInstance(serviceType);
if (service is IRequiresRequest serviceRequiresContext)
{
serviceRequiresContext.Request = req;
}
// Executes the service and returns the result
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
}
}
}

@ -1,230 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
namespace Emby.Server.Implementations.Services
{
public static class ServiceExecExtensions
{
public static string[] AllVerbs = new[] {
"OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
"VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
"MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
"ORDERPATCH", // RFC 3648
"ACL", // RFC 3744
"PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
"SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
"BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
"POLL", "SUBSCRIBE", "UNSUBSCRIBE"
};
public static List<MethodInfo> GetActions(this Type serviceType)
{
var list = new List<MethodInfo>();
foreach (var mi in serviceType.GetRuntimeMethods())
{
if (!mi.IsPublic)
{
continue;
}
if (mi.IsStatic)
{
continue;
}
if (mi.GetParameters().Length != 1)
{
continue;
}
var actionName = mi.Name;
if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
{
continue;
}
list.Add(mi);
}
return list;
}
}
internal static class ServiceExecGeneral
{
private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
{
foreach (var actionCtx in actions)
{
if (execMap.ContainsKey(actionCtx.Id))
{
continue;
}
execMap[actionCtx.Id] = actionCtx;
}
}
public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
{
var actionName = request.Verb ?? "POST";
if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext))
{
if (actionContext.RequestFilters != null)
{
foreach (var requestFilter in actionContext.RequestFilters)
{
requestFilter.RequestFilter(request, request.Response, requestDto);
if (request.Response.HasStarted)
{
Task.FromResult<object>(null);
}
}
}
var response = actionContext.ServiceAction(instance, requestDto);
if (response is Task taskResponse)
{
return GetTaskResult(taskResponse);
}
return Task.FromResult(response);
}
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
throw new NotImplementedException(
string.Format(
CultureInfo.InvariantCulture,
"Could not find method named {1}({0}) or Any({0}) on Service {2}",
requestDto.GetType().GetMethodName(),
expectedMethodName,
serviceType.GetMethodName()));
}
private static async Task<object> GetTaskResult(Task task)
{
try
{
if (task is Task<object> taskObject)
{
return await taskObject.ConfigureAwait(false);
}
await task.ConfigureAwait(false);
var type = task.GetType().GetTypeInfo();
if (!type.IsGenericType)
{
return null;
}
var resultProperty = type.GetDeclaredProperty("Result");
if (resultProperty == null)
{
return null;
}
var result = resultProperty.GetValue(task);
// hack alert
if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1)
{
return null;
}
return result;
}
catch (TypeAccessException)
{
return null; // return null for void Task's
}
}
public static List<ServiceMethod> Reset(Type serviceType)
{
var actions = new List<ServiceMethod>();
foreach (var mi in serviceType.GetActions())
{
var actionName = mi.Name;
var args = mi.GetParameters();
var requestType = args[0].ParameterType;
var actionCtx = new ServiceMethod
{
Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
};
actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
var reqFilters = new List<IHasRequestFilter>();
foreach (var attr in mi.GetCustomAttributes(true))
{
if (attr is IHasRequestFilter hasReqFilter)
{
reqFilters.Add(hasReqFilter);
}
}
if (reqFilters.Count > 0)
{
actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
}
actions.Add(actionCtx);
}
return actions;
}
private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
{
var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
var serviceStrong = Expression.Convert(serviceParam, serviceType);
var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
Expression callExecute = Expression.Call(
serviceStrong, mi, requestDtoStrong);
if (mi.ReturnType != typeof(void))
{
var executeFunc = Expression.Lambda<ActionInvokerFn>(
callExecute,
serviceParam,
requestDtoParam).Compile();
return executeFunc;
}
else
{
var executeFunc = Expression.Lambda<VoidActionInvokerFn>(
callExecute,
serviceParam,
requestDtoParam).Compile();
return (service, request) =>
{
executeFunc(service, request);
return null;
};
}
}
}
}

@ -1,212 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Net.Mime;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Services
{
public class ServiceHandler
{
private RestPath _restPath;
private string _responseContentType;
internal ServiceHandler(RestPath restPath, string responseContentType)
{
_restPath = restPath;
_responseContentType = responseContentType;
}
protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
{
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
{
var deserializer = RequestHelper.GetRequestReader(host, contentType);
if (deserializer != null)
{
return deserializer.Invoke(requestType, httpReq.InputStream);
}
}
return Task.FromResult(host.CreateInstance(requestType));
}
public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
{
contentType = null;
var pos = pathInfo.LastIndexOf('.');
if (pos != -1)
{
var format = pathInfo.AsSpan().Slice(pos + 1);
contentType = GetFormatContentType(format);
if (contentType != null)
{
pathInfo = pathInfo.Substring(0, pos);
}
}
return pathInfo;
}
private static string GetFormatContentType(ReadOnlySpan<char> format)
{
if (format.Equals("json", StringComparison.Ordinal))
{
return MediaTypeNames.Application.Json;
}
else if (format.Equals("xml", StringComparison.Ordinal))
{
return MediaTypeNames.Application.Xml;
}
return null;
}
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
{
httpReq.Items["__route"] = _restPath;
if (_responseContentType != null)
{
httpReq.ResponseContentType = _responseContentType;
}
var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
httpRes.HttpContext.SetServiceStackRequest(httpReq);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters
foreach (var responseFilter in httpHost.ResponseFilters)
{
responseFilter(httpReq, httpRes, response);
}
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
}
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
{
var requestType = restPath.RequestType;
if (RequireqRequestStream(requestType))
{
// Used by IRequiresRequestStream
var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request);
var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType));
var rawReq = (IRequiresRequestStream)request;
rawReq.RequestStream = httpReq.InputStream;
return rawReq;
}
else
{
var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request);
var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false);
return CreateRequest(httpReq, restPath, requestParams, requestDto);
}
}
public static bool RequireqRequestStream(Type requestType)
{
var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
}
public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
{
var pathInfo = !restPath.IsWildCardPath
? GetSanitizedPathInfo(httpReq.PathInfo, out _)
: httpReq.PathInfo;
return restPath.CreateRequest(pathInfo, requestParams, requestDto);
}
/// <summary>
/// Duplicate Params are given a unique key by appending a #1 suffix
/// </summary>
private static Dictionary<string, string> GetRequestParams(HttpRequest request)
{
var map = new Dictionary<string, string>();
foreach (var pair in request.Query)
{
var values = pair.Value;
if (values.Count == 1)
{
map[pair.Key] = values[0];
}
else
{
for (var i = 0; i < values.Count; i++)
{
map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
}
}
}
if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
&& request.HasFormContentType)
{
foreach (var pair in request.Form)
{
var values = pair.Value;
if (values.Count == 1)
{
map[pair.Key] = values[0];
}
else
{
for (var i = 0; i < values.Count; i++)
{
map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
}
}
}
}
return map;
}
private static bool IsMethod(string method, string expected)
=> string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Duplicate params have their values joined together in a comma-delimited string.
/// </summary>
private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request)
{
var map = new Dictionary<string, string>();
foreach (var pair in request.Query)
{
map[pair.Key] = pair.Value;
}
if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
&& request.HasFormContentType)
{
foreach (var pair in request.Form)
{
map[pair.Key] = pair.Value;
}
}
return map;
}
}
}

@ -1,20 +0,0 @@
#pragma warning disable CS1591
using System;
namespace Emby.Server.Implementations.Services
{
public class ServiceMethod
{
public string Id { get; set; }
public ActionInvokerFn ServiceAction { get; set; }
public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
public static string Key(Type serviceType, string method, string requestDtoName)
{
return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName;
}
}
}

@ -1,550 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.Services
{
public class RestPath
{
private const string WildCard = "*";
private const char WildCardChar = '*';
private const string PathSeperator = "/";
private const char PathSeperatorChar = '/';
private const char ComponentSeperator = '.';
private const string VariablePrefix = "{";
private readonly bool[] componentsWithSeparators;
private readonly string restPath;
public bool IsWildCardPath { get; private set; }
private readonly string[] literalsToMatch;
private readonly string[] variablesNames;
private readonly bool[] isWildcard;
private readonly int wildcardCount = 0;
internal static string[] IgnoreAttributesNamed = new[]
{
nameof(JsonIgnoreAttribute)
};
private static Type _excludeType = typeof(Stream);
public int VariableArgsCount { get; set; }
/// <summary>
/// The number of segments separated by '/' determinable by path.Split('/').Length
/// e.g. /path/to/here.ext == 3
/// </summary>
public int PathComponentsCount { get; set; }
/// <summary>
/// Gets or sets the total number of segments after subparts have been exploded ('.')
/// e.g. /path/to/here.ext == 4.
/// </summary>
public int TotalComponentsCount { get; set; }
public string[] Verbs { get; private set; }
public Type RequestType { get; private set; }
public Type ServiceType { get; private set; }
public string Path => this.restPath;
public string Summary { get; private set; }
public string Description { get; private set; }
public bool IsHidden { get; private set; }
public static string[] GetPathPartsForMatching(string pathInfo)
{
return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
}
public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
{
var hashPrefix = pathPartsForMatching.Length + PathSeperator;
return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
}
public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
{
const string HashPrefix = WildCard + PathSeperator;
return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching);
}
private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
{
var list = new List<string>();
foreach (var part in pathPartsForMatching)
{
list.Add(hashPrefix + part);
if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1)
{
continue;
}
var subParts = part.Split(ComponentSeperator);
foreach (var subPart in subParts)
{
list.Add(hashPrefix + subPart);
}
}
return list;
}
public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null)
{
this.RequestType = requestType;
this.ServiceType = serviceType;
this.Summary = summary;
this.IsHidden = isHidden;
this.Description = description;
this.restPath = path;
this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
var componentsList = new List<string>();
// We only split on '.' if the restPath has them. Allows for /{action}.{type}
var hasSeparators = new List<bool>();
foreach (var component in this.restPath.Split(PathSeperatorChar))
{
if (string.IsNullOrEmpty(component))
{
continue;
}
if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
&& component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1)
{
hasSeparators.Add(true);
componentsList.AddRange(component.Split(ComponentSeperator));
}
else
{
hasSeparators.Add(false);
componentsList.Add(component);
}
}
var components = componentsList.ToArray();
this.TotalComponentsCount = components.Length;
this.literalsToMatch = new string[this.TotalComponentsCount];
this.variablesNames = new string[this.TotalComponentsCount];
this.isWildcard = new bool[this.TotalComponentsCount];
this.componentsWithSeparators = hasSeparators.ToArray();
this.PathComponentsCount = this.componentsWithSeparators.Length;
string firstLiteralMatch = null;
for (var i = 0; i < components.Length; i++)
{
var component = components[i];
if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
{
var variableName = component.Substring(1, component.Length - 2);
if (variableName[variableName.Length - 1] == WildCardChar)
{
this.isWildcard[i] = true;
variableName = variableName.Substring(0, variableName.Length - 1);
}
this.variablesNames[i] = variableName;
this.VariableArgsCount++;
}
else
{
this.literalsToMatch[i] = component.ToLowerInvariant();
if (firstLiteralMatch == null)
{
firstLiteralMatch = this.literalsToMatch[i];
}
}
}
for (var i = 0; i < components.Length - 1; i++)
{
if (!this.isWildcard[i])
{
continue;
}
if (this.literalsToMatch[i + 1] == null)
{
throw new ArgumentException(
"A wildcard path component must be at the end of the path or followed by a literal path component.");
}
}
this.wildcardCount = this.isWildcard.Length;
this.IsWildCardPath = this.wildcardCount > 0;
this.FirstMatchHashKey = !this.IsWildCardPath
? this.PathComponentsCount + PathSeperator + firstLiteralMatch
: WildCardChar + PathSeperator + firstLiteralMatch;
this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
_propertyNamesMap = new HashSet<string>(
GetSerializableProperties(RequestType).Select(x => x.Name),
StringComparer.OrdinalIgnoreCase);
}
internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
{
foreach (var prop in GetPublicProperties(type))
{
if (prop.GetMethod == null
|| _excludeType == prop.PropertyType)
{
continue;
}
var ignored = false;
foreach (var attr in prop.GetCustomAttributes(true))
{
if (IgnoreAttributesNamed.Contains(attr.GetType().Name))
{
ignored = true;
break;
}
}
if (!ignored)
{
yield return prop;
}
}
}
private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
{
if (type.IsInterface)
{
var propertyInfos = new List<PropertyInfo>();
var considered = new List<Type>()
{
type
};
var queue = new Queue<Type>();
queue.Enqueue(type);
while (queue.Count > 0)
{
var subType = queue.Dequeue();
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
{
if (considered.Contains(subInterface))
{
continue;
}
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
var newPropertyInfos = GetTypesPublicProperties(subType)
.Where(x => !propertyInfos.Contains(x));
propertyInfos.InsertRange(0, newPropertyInfos);
}
return propertyInfos;
}
return GetTypesPublicProperties(type)
.Where(x => x.GetIndexParameters().Length == 0);
}
private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
{
foreach (var pi in subType.GetRuntimeProperties())
{
var mi = pi.GetMethod ?? pi.SetMethod;
if (mi != null && mi.IsStatic)
{
continue;
}
yield return pi;
}
}
/// <summary>
/// Provide for quick lookups based on hashes that can be determined from a request url.
/// </summary>
public string FirstMatchHashKey { get; private set; }
private readonly StringMapTypeDeserializer typeDeserializer;
private readonly HashSet<string> _propertyNamesMap;
public int MatchScore(string httpMethod, string[] withPathInfoParts)
{
var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount);
if (!isMatch)
{
return -1;
}
// Routes with least wildcard matches get the highest score
var score = Math.Max(100 - wildcardMatchCount, 1) * 1000
// Routes with less variable (and more literal) matches
+ Math.Max(10 - VariableArgsCount, 1) * 100;
// Exact verb match is better than ANY
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
{
score += 10;
}
else
{
score += 1;
}
return score;
}
/// <summary>
/// For performance withPathInfoParts should already be a lower case string
/// to minimize redundant matching operations.
/// </summary>
public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
{
wildcardMatchCount = 0;
if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
{
return false;
}
if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if (!ExplodeComponents(ref withPathInfoParts))
{
return false;
}
if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
{
return false;
}
int pathIx = 0;
for (var i = 0; i < this.TotalComponentsCount; i++)
{
if (this.isWildcard[i])
{
if (i < this.TotalComponentsCount - 1)
{
// Continue to consume up until a match with the next literal
while (pathIx < withPathInfoParts.Length
&& !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
{
pathIx++;
wildcardMatchCount++;
}
// Ensure there are still enough parts left to match the remainder
if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
{
return false;
}
}
else
{
// A wildcard at the end matches the remainder of path
wildcardMatchCount += withPathInfoParts.Length - pathIx;
pathIx = withPathInfoParts.Length;
}
}
else
{
var literalToMatch = this.literalsToMatch[i];
if (literalToMatch == null)
{
// Matching an ordinary (non-wildcard) variable consumes a single part
pathIx++;
continue;
}
if (withPathInfoParts.Length <= pathIx
|| !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
pathIx++;
}
}
return pathIx == withPathInfoParts.Length;
}
private bool ExplodeComponents(ref string[] withPathInfoParts)
{
var totalComponents = new List<string>();
for (var i = 0; i < withPathInfoParts.Length; i++)
{
var component = withPathInfoParts[i];
if (string.IsNullOrEmpty(component))
{
continue;
}
if (this.PathComponentsCount != this.TotalComponentsCount
&& this.componentsWithSeparators[i])
{
var subComponents = component.Split(ComponentSeperator);
if (subComponents.Length < 2)
{
return false;
}
totalComponents.AddRange(subComponents);
}
else
{
totalComponents.Add(component);
}
}
withPathInfoParts = totalComponents.ToArray();
return true;
}
public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
{
var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
ExplodeComponents(ref requestComponents);
if (requestComponents.Length != this.TotalComponentsCount)
{
var isValidWildCardPath = this.IsWildCardPath
&& requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
if (!isValidWildCardPath)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
pathInfo,
this.restPath));
}
}
var requestKeyValuesMap = new Dictionary<string, string>();
var pathIx = 0;
for (var i = 0; i < this.TotalComponentsCount; i++)
{
var variableName = this.variablesNames[i];
if (variableName == null)
{
pathIx++;
continue;
}
if (!this._propertyNamesMap.Contains(variableName))
{
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
{
pathIx++;
continue;
}
throw new ArgumentException("Could not find property "
+ variableName + " on " + RequestType.GetMethodName());
}
var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch
if (value != null && this.isWildcard[i])
{
if (i == this.TotalComponentsCount - 1)
{
// Wildcard at end of path definition consumes all the rest
var sb = new StringBuilder();
sb.Append(value);
for (var j = pathIx + 1; j < requestComponents.Length; j++)
{
sb.Append(PathSeperatorChar)
.Append(requestComponents[j]);
}
value = sb.ToString();
}
else
{
// Wildcard in middle of path definition consumes up until it
// hits a match for the next element in the definition (which must be a literal)
// It may consume 0 or more path parts
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
var sb = new StringBuilder(value);
pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
sb.Append(PathSeperatorChar)
.Append(requestComponents[pathIx++]);
}
value = sb.ToString();
}
else
{
value = null;
}
}
}
else
{
// Variable consumes single path item
pathIx++;
}
requestKeyValuesMap[variableName] = value;
}
if (queryStringAndFormData != null)
{
// Query String and form data can override variable path matches
// path variables < query string < form data
foreach (var name in queryStringAndFormData)
{
requestKeyValuesMap[name.Key] = name.Value;
}
}
return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
}
public class RestPathMap : SortedDictionary<string, List<RestPath>>
{
public RestPathMap() : base(StringComparer.OrdinalIgnoreCase)
{
}
}
}
}

@ -1,118 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Reflection;
using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
/// <summary>
/// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
/// </summary>
public class StringMapTypeDeserializer
{
internal class PropertySerializerEntry
{
public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
{
PropertySetFn = propertySetFn;
PropertyParseStringFn = propertyParseStringFn;
PropertyType = propertyType;
}
public Action<object, object> PropertySetFn { get; private set; }
public Func<string, object> PropertyParseStringFn { get; private set; }
public Type PropertyType { get; private set; }
}
private readonly Type type;
private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap
= new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase);
public Func<string, object> GetParseFn(Type propertyType)
{
if (propertyType == typeof(string))
{
return s => s;
}
return _GetParseFn(propertyType);
}
private readonly Func<Type, object> _CreateInstanceFn;
private readonly Func<Type, Func<string, object>> _GetParseFn;
public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type)
{
_CreateInstanceFn = createInstanceFn;
_GetParseFn = getParseFn;
this.type = type;
foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
{
var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
var propertyType = propertyInfo.PropertyType;
var propertyParseStringFn = GetParseFn(propertyType);
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
propertySetterMap[propertyInfo.Name] = propertySerializer;
}
}
public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
{
PropertySerializerEntry propertySerializerEntry = null;
if (instance == null)
{
instance = _CreateInstanceFn(type);
}
foreach (var pair in keyValuePairs)
{
string propertyName = pair.Key;
string propertyTextValue = pair.Value;
if (propertyTextValue == null
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|| propertySerializerEntry.PropertySetFn == null)
{
continue;
}
if (propertySerializerEntry.PropertyType == typeof(bool))
{
// InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
}
var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
if (value == null)
{
continue;
}
propertySerializerEntry.PropertySetFn(instance, value);
}
return instance;
}
}
internal static class TypeAccessor
{
public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo)
{
if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
{
return null;
}
var setMethodInfo = propertyInfo.SetMethod;
return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
}
}
}

@ -1,27 +0,0 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
/// <summary>
/// Donated by Ivan Korneliuk from his post:
/// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
///
/// Modified to only allow using routes matching the supplied HTTP Verb.
/// </summary>
public static class UrlExtensions
{
public static string GetMethodName(this Type type)
{
var typeName = type.FullName != null // can be null, e.g. generic types
? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
.Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
.Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
: type.Name;
return type.IsGenericParameter ? "'" + typeName : typeName;
}
}
}

@ -1037,7 +1037,7 @@ namespace Emby.Server.Implementations.Session
var generalCommand = new GeneralCommand
{
Name = GeneralCommandType.DisplayMessage.ToString()
Name = GeneralCommandType.DisplayMessage
};
generalCommand.Arguments["Header"] = command.Header;
@ -1268,7 +1268,7 @@ namespace Emby.Server.Implementations.Session
{
var generalCommand = new GeneralCommand
{
Name = GeneralCommandType.DisplayContent.ToString(),
Name = GeneralCommandType.DisplayContent,
Arguments =
{
["ItemId"] = command.ItemId,

@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session
private readonly ILogger<SessionWebSocketListener> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IHttpServer _httpServer;
private readonly IWebSocketManager _webSocketManager;
/// <summary>
/// The KeepAlive cancellation token.
@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="httpServer">The HTTP server.</param>
/// <param name="webSocketManager">The HTTP server.</param>
public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
IHttpServer httpServer)
IWebSocketManager webSocketManager)
{
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
_httpServer = httpServer;
_webSocketManager = webSocketManager;
httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
}
private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public void Dispose()
{
_httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
_webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
StopKeepAlive();
}

@ -1,248 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Mime;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
namespace Emby.Server.Implementations.SocketSharp
{
public class WebSocketSharpRequest : IHttpRequest
{
private const string FormUrlEncoded = "application/x-www-form-urlencoded";
private const string MultiPartFormData = "multipart/form-data";
private const string Soap11 = "text/xml; charset=utf-8";
private string _remoteIp;
private Dictionary<string, object> _items;
private string _responseContentType;
public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName)
{
this.OperationName = operationName;
this.Request = httpRequest;
this.Response = httpResponse;
}
public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString();
public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString();
public HttpRequest Request { get; }
public HttpResponse Response { get; }
public string OperationName { get; set; }
public string RawUrl => Request.GetEncodedPathAndQuery();
public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/');
public string RemoteIp
{
get
{
if (_remoteIp != null)
{
return _remoteIp;
}
IPAddress ip;
// "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
// (if the server is behind a reverse proxy for example)
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
{
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
{
ip = Request.HttpContext.Connection.RemoteIpAddress;
// Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
ip ??= IPAddress.Loopback;
}
}
return _remoteIp = NormalizeIp(ip).ToString();
}
}
public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>());
public string ResponseContentType
{
get =>
_responseContentType
?? (_responseContentType = GetResponseContentType(Request));
set => _responseContentType = value;
}
public string PathInfo => Request.Path.Value;
public string UserAgent => Request.Headers[HeaderNames.UserAgent];
public IHeaderDictionary Headers => Request.Headers;
public IQueryCollection QueryString => Request.Query;
public bool IsLocal =>
(Request.HttpContext.Connection.LocalIpAddress == null
&& Request.HttpContext.Connection.RemoteIpAddress == null)
|| Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
public string HttpMethod => Request.Method;
public string Verb => HttpMethod;
public string ContentType => Request.ContentType;
public Uri UrlReferrer => Request.GetTypedHeaders().Referer;
public Stream InputStream => Request.Body;
public long ContentLength => Request.ContentLength ?? 0;
private string GetHeader(string name) => Request.Headers[name].ToString();
private static IPAddress NormalizeIp(IPAddress ip)
{
if (ip.IsIPv4MappedToIPv6)
{
return ip.MapToIPv4();
}
return ip;
}
public static string GetResponseContentType(HttpRequest httpReq)
{
var specifiedContentType = GetQueryStringContentType(httpReq);
if (!string.IsNullOrEmpty(specifiedContentType))
{
return specifiedContentType;
}
const string ServerDefaultContentType = MediaTypeNames.Application.Json;
var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
string defaultContentType = null;
if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
{
defaultContentType = ServerDefaultContentType;
}
var acceptsAnything = false;
var hasDefaultContentType = defaultContentType != null;
if (acceptContentTypes != null)
{
foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes)
{
ReadOnlySpan<char> contentType = acceptsType;
var index = contentType.IndexOf(';');
if (index != -1)
{
contentType = contentType.Slice(0, index);
}
contentType = contentType.Trim();
acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
if (acceptsAnything)
{
break;
}
}
if (acceptsAnything)
{
if (hasDefaultContentType)
{
return defaultContentType;
}
else
{
return ServerDefaultContentType;
}
}
}
if (acceptContentTypes == null && httpReq.ContentType == Soap11)
{
return Soap11;
}
// We could also send a '406 Not Acceptable', but this is allowed also
return ServerDefaultContentType;
}
public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
{
if (contentTypes == null || request.ContentType == null)
{
return false;
}
foreach (var contentType in contentTypes)
{
if (IsContentType(request, contentType))
{
return true;
}
}
return false;
}
public static bool IsContentType(HttpRequest request, string contentType)
{
return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
}
private static string GetQueryStringContentType(HttpRequest httpReq)
{
ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
if (format == ReadOnlySpan<char>.Empty)
{
const int FormatMaxLength = 4;
ReadOnlySpan<char> pi = httpReq.Path.ToString();
if (pi == null || pi.Length <= FormatMaxLength)
{
return null;
}
if (pi[0] == '/')
{
pi = pi.Slice(1);
}
format = pi.LeftPart('/');
if (format.Length > FormatMaxLength)
{
return null;
}
}
format = format.LeftPart('.');
if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
{
return "application/json";
}
else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
{
return "application/xml";
}
return null;
}
}
}

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

Loading…
Cancel
Save