Merge remote-tracking branch 'upstream/master' into fonts

This commit is contained in:
nyanmisaka 2020-08-19 16:56:53 +08:00
commit aa75755480
555 changed files with 31288 additions and 26854 deletions

View File

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

View File

@ -35,7 +35,6 @@ jobs:
steps: steps:
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment' - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile' displayName: 'Build Dockerfile'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)' - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
displayName: 'Run Dockerfile (unstable)' displayName: 'Run Dockerfile (unstable)'
@ -47,14 +46,19 @@ jobs:
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@1
displayName: 'Publish Release' displayName: 'Publish Release'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs: inputs:
targetPath: '$(Build.SourcesDirectory)/deployment/dist' targetPath: '$(Build.SourcesDirectory)/deployment/dist'
artifactName: 'jellyfin-server-$(BuildConfiguration)' artifactName: 'jellyfin-server-$(BuildConfiguration)'
- task: SSH@0
displayName: 'Create target directory on repository server'
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- task: CopyFilesOverSSH@0 - task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server' displayName: 'Upload artifacts to repository server'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist' sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
@ -76,7 +80,15 @@ jobs:
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: 0.0.0
steps: 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')
- task: Docker@2 - task: Docker@2
displayName: 'Push Unstable Image' displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@ -101,9 +113,10 @@ jobs:
containerRegistry: Docker Hub containerRegistry: Docker Hub
tags: | tags: |
stable-$(Build.BuildNumber)-$(BuildConfiguration) stable-$(Build.BuildNumber)-$(BuildConfiguration)
stable-$(BuildConfiguration) $(JellyfinVersion)-$(BuildConfiguration)
- job: CollectArtifacts - job: CollectArtifacts
timeoutInMinutes: 10
displayName: 'Collect Artifacts' displayName: 'Collect Artifacts'
dependsOn: dependsOn:
- BuildPackage - BuildPackage
@ -119,13 +132,35 @@ jobs:
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
runOptions: 'inline' runOptions: 'commands'
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable' commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
- task: SSH@0 - task: SSH@0
displayName: 'Update Stable Repository' displayName: 'Update Stable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
runOptions: 'inline' runOptions: 'commands'
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)' commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NuGetCommand@2
inputs:
command: 'pack'
packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
packDestination: '$(Build.ArtifactStagingDirectory)'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
includeNugetOrg: 'true'

View File

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

14
.vscode/launch.json vendored
View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ using System.Xml;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;

View File

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

View File

@ -364,7 +364,8 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
} }
var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container, var mediaProfile = _profile.GetVideoMediaProfile(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate, streamInfo.TargetAudioBitrate,

View File

@ -122,15 +122,15 @@ namespace Emby.Dlna
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used."); builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
_logger.LogInformation(builder.ToString()); _logger.LogInformation(builder.ToString());
} }
@ -387,7 +387,7 @@ namespace Emby.Dlna
foreach (var name in _assembly.GetManifestResourceNames()) foreach (var name in _assembly.GetManifestResourceNames())
{ {
if (!name.StartsWith(namespaceName)) if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
{ {
continue; continue;
} }
@ -406,7 +406,7 @@ namespace Emby.Dlna
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
await stream.CopyToAsync(fileStream); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }
} }
} }
@ -509,7 +509,7 @@ namespace Emby.Dlna
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
} }
class InternalProfileInfo private class InternalProfileInfo
{ {
internal DeviceProfileInfo Info { get; set; } internal DeviceProfileInfo Info { get; set; }

View File

@ -152,11 +152,15 @@ namespace Emby.Dlna.Eventing
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
foreach (var key in stateVariables.Keys) foreach (var key in stateVariables.Keys)
{ {
builder.Append("<e:property>"); builder.Append("<e:property>")
builder.Append("<" + key + ">"); .Append('<')
builder.Append(stateVariables[key]); .Append(key)
builder.Append("</" + key + ">"); .Append('>')
builder.Append("</e:property>"); .Append(stateVariables[key])
.Append("</")
.Append(key)
.Append('>')
.Append("</e:property>");
} }
builder.Append("</e:propertyset>"); builder.Append("</e:propertyset>");

View File

@ -54,11 +54,11 @@ namespace Emby.Dlna.Main
private SsdpDevicePublisher _publisher; private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer; private ISsdpCommunicationsServer _communicationsServer;
internal IContentDirectory ContentDirectory { get; private set; } public IContentDirectory ContentDirectory { get; private set; }
internal IConnectionManager ConnectionManager { get; private set; } public IConnectionManager ConnectionManager { get; private set; }
internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public static DlnaEntryPoint Current; public static DlnaEntryPoint Current;

View File

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
return string.Empty; return string.Empty;
} }
return DescriptionXmlBuilder.Escape(value); return SecurityElement.Escape(value);
} }
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)

View File

@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
public void AddXmlRootAttribute(string name, string value) public void AddXmlRootAttribute(string name, string value)
{ {
var atts = XmlRootAttributes ?? new XmlAttribute[] { }; var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
var list = atts.ToList(); var list = atts.ToList();
list.Add(new XmlAttribute list.Add(new XmlAttribute

View File

@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles
}, },
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles
}, },
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -37,7 +38,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View File

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security;
using System.Text; using System.Text;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
foreach (var att in attributes) foreach (var att in attributes)
{ {
builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value); builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
} }
builder.Append(">"); builder.Append('>');
builder.Append("<specVersion>"); builder.Append("<specVersion>");
builder.Append("<major>1</major>"); builder.Append("<major>1</major>");
@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
if (!EnableAbsoluteUrls) if (!EnableAbsoluteUrls)
{ {
builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>"); builder.Append("<URLBase>")
.Append(SecurityElement.Escape(_serverAddress))
.Append("</URLBase>");
} }
AppendDeviceInfo(builder); AppendDeviceInfo(builder);
@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
AppendIconList(builder); AppendIconList(builder);
builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>"); builder.Append("<presentationURL>")
.Append(SecurityElement.Escape(_serverAddress))
.Append("/web/index.html</presentationURL>");
AppendServiceList(builder); AppendServiceList(builder);
builder.Append("</device>"); builder.Append("</device>");
} }
private static readonly char[] s_escapeChars = new char[]
{
'<',
'>',
'"',
'\'',
'&'
};
private static readonly string[] s_escapeStringPairs = new[]
{
"<",
"&lt;",
">",
"&gt;",
"\"",
"&quot;",
"'",
"&apos;",
"&",
"&amp;"
};
private static string GetEscapeSequence(char c)
{
int num = s_escapeStringPairs.Length;
for (int i = 0; i < num; i += 2)
{
string text = s_escapeStringPairs[i];
string result = s_escapeStringPairs[i + 1];
if (text[0] == c)
{
return result;
}
}
return c.ToString(CultureInfo.InvariantCulture);
}
/// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
/// <returns>The input string with invalid characters replaced.</returns>
/// <param name="str">The string within which to escape invalid characters. </param>
public static string Escape(string str)
{
if (str == null)
{
return null;
}
StringBuilder stringBuilder = null;
int length = str.Length;
int num = 0;
while (true)
{
int num2 = str.IndexOfAny(s_escapeChars, num);
if (num2 == -1)
{
break;
}
if (stringBuilder == null)
{
stringBuilder = new StringBuilder();
}
stringBuilder.Append(str, num, num2 - num);
stringBuilder.Append(GetEscapeSequence(str[num2]));
num = num2 + 1;
}
if (stringBuilder == null)
{
return str;
}
stringBuilder.Append(str, num, length - num);
return stringBuilder.ToString();
}
private void AppendDeviceProperties(StringBuilder builder) private void AppendDeviceProperties(StringBuilder builder)
{ {
builder.Append("<dlna:X_DLNACAP/>"); builder.Append("<dlna:X_DLNACAP/>");
@ -187,32 +113,54 @@ namespace Emby.Dlna.Server
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"); builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>"); builder.Append("<friendlyName>")
builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>"); .Append(SecurityElement.Escape(GetFriendlyName()))
builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>"); .Append("</friendlyName>");
builder.Append("<manufacturer>")
.Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
.Append("</manufacturer>");
builder.Append("<manufacturerURL>")
.Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
.Append("</manufacturerURL>");
builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>"); builder.Append("<modelDescription>")
builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>"); .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
.Append("</modelDescription>");
builder.Append("<modelName>")
.Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
.Append("</modelName>");
builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>"); builder.Append("<modelNumber>")
builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>"); .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
.Append("</modelNumber>");
builder.Append("<modelURL>")
.Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
.Append("</modelURL>");
if (string.IsNullOrEmpty(_profile.SerialNumber)) if (string.IsNullOrEmpty(_profile.SerialNumber))
{ {
builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>"); builder.Append("<serialNumber>")
.Append(SecurityElement.Escape(_serverId))
.Append("</serialNumber>");
} }
else else
{ {
builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>"); builder.Append("<serialNumber>")
.Append(SecurityElement.Escape(_profile.SerialNumber))
.Append("</serialNumber>");
} }
builder.Append("<UPC/>"); builder.Append("<UPC/>");
builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>"); builder.Append("<UDN>uuid:")
.Append(SecurityElement.Escape(_serverUdn))
.Append("</UDN>");
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags)) if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
{ {
builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>"); builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
.Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
.Append("</av:aggregationFlags>");
} }
} }
@ -250,11 +198,21 @@ namespace Emby.Dlna.Server
{ {
builder.Append("<icon>"); builder.Append("<icon>");
builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>"); builder.Append("<mimetype>")
builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>"); .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>"); .Append("</mimetype>");
builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>"); builder.Append("<width>")
builder.Append("<url>" + BuildUrl(icon.Url) + "</url>"); .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
.Append("</width>");
builder.Append("<height>")
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
.Append("</height>");
builder.Append("<depth>")
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
.Append("</depth>");
builder.Append("<url>")
.Append(BuildUrl(icon.Url))
.Append("</url>");
builder.Append("</icon>"); builder.Append("</icon>");
} }
@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
{ {
builder.Append("<service>"); builder.Append("<service>");
builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>"); builder.Append("<serviceType>")
builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>"); .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>"); .Append("</serviceType>");
builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>"); builder.Append("<serviceId>")
builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>"); .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
.Append("</serviceId>");
builder.Append("<SCPDURL>")
.Append(BuildUrl(service.ScpdUrl))
.Append("</SCPDURL>");
builder.Append("<controlURL>")
.Append(BuildUrl(service.ControlUrl))
.Append("</controlURL>");
builder.Append("<eventSubURL>")
.Append(BuildUrl(service.EventSubUrl))
.Append("</eventSubURL>");
builder.Append("</service>"); builder.Append("</service>");
} }
@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + url; url = _serverAddress.TrimEnd('/') + url;
} }
return Escape(url); return SecurityElement.Escape(url);
} }
private IEnumerable<DeviceIcon> GetIcons() private IEnumerable<DeviceIcon> GetIcons()

View File

@ -1,9 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.Security;
using System.Text; using System.Text;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
namespace Emby.Dlna.Service namespace Emby.Dlna.Service
{ {
@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
{ {
builder.Append("<action>"); builder.Append("<action>");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<argumentList>"); builder.Append("<argumentList>");
@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
{ {
builder.Append("<argument>"); builder.Append("<argument>");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>"); .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>"); .Append("</name>");
builder.Append("<direction>")
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
.Append("</direction>");
builder.Append("<relatedStateVariable>")
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
.Append("</relatedStateVariable>");
builder.Append("</argument>"); builder.Append("</argument>");
} }
@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
{ {
var sendEvents = item.SendsEvents ? "yes" : "no"; var sendEvents = item.SendsEvents ? "yes" : "no";
builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">"); builder.Append("<stateVariable sendEvents=\"")
.Append(sendEvents)
.Append("\">");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>"); .Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<dataType>")
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
.Append("</dataType>");
if (item.AllowedValues.Length > 0) if (item.AllowedValues.Length > 0)
{ {
builder.Append("<allowedValueList>"); builder.Append("<allowedValueList>");
foreach (var allowedValue in item.AllowedValues) foreach (var allowedValue in item.AllowedValues)
{ {
builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>"); builder.Append("<allowedValue>")
.Append(SecurityElement.Escape(allowedValue))
.Append("</allowedValue>");
} }
builder.Append("</allowedValueList>"); builder.Append("</allowedValueList>");

View File

@ -448,21 +448,21 @@ namespace Emby.Drawing
/// or /// or
/// filename. /// filename.
/// </exception> /// </exception>
public string GetCachePath(string path, string filename) public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
{ {
if (string.IsNullOrEmpty(path)) if (path.IsEmpty)
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentException("Path can't be empty.", nameof(path));
} }
if (string.IsNullOrEmpty(filename)) if (path.IsEmpty)
{ {
throw new ArgumentNullException(nameof(filename)); throw new ArgumentException("Filename can't be empty.", nameof(filename));
} }
var prefix = filename.Substring(0, 1); var prefix = filename.Slice(0, 1);
return Path.Combine(path, prefix, filename); return Path.Join(path, prefix, filename);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -136,8 +136,8 @@ namespace Emby.Naming.Common
CleanDateTimes = new[] CleanDateTimes = new[]
{ {
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*", @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
}; };
CleanStrings = new[] CleanStrings = new[]
@ -277,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives, // This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first. // so we make sure this one gets tested first.
// "Foo Bar 889" // "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$") new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
{ {
IsNamed = true IsNamed = true
}, },
@ -300,32 +300,32 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** End Kodi Standard Naming
// [bar] Foo - 1 [baz] // [bar] Foo - 1 [baz]
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$") new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
// "01.avi" // "01.avi"
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$") new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -335,34 +335,34 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"([0-9]+)-([0-9]+)"), new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi" // "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01.blah.avi" // "01.blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah" // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01 episode title.avi" // "01 episode title.avi"
new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$") new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "Episode 16", "Episode 16 - Title" // "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -625,17 +625,17 @@ namespace Emby.Naming.Common
AudioBookPartsExpressions = new[] AudioBookPartsExpressions = new[]
{ {
// Detect specified chapters, like CH 01 // Detect specified chapters, like CH 01
@"ch(?:apter)?[\s_-]?(?<chapter>\d+)", @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02 // Detect specified parts, like Part 02
@"p(?:ar)?t[\s_-]?(?<part>\d+)", @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
// Chapter is often beginning of filename // Chapter is often beginning of filename
@"^(?<chapter>\d+)", "^(?<chapter>[0-9]+)",
// Part if often ending of filename // Part if often ending of filename
@"(?<part>\d+)$", "(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part) // Sometimes named as 0001_005 (chapter_part)
@"(?<chapter>\d+)_(?<part>\d+)", "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number. // Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>\d+)" @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
}; };
var extensions = VideoFileExtensions.ToList(); var extensions = VideoFileExtensions.ToList();
@ -675,16 +675,16 @@ namespace Emby.Naming.Common
MultipleEpisodeExpressions = new string[] MultipleEpisodeExpressions = new string[]
{ {
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$" @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
}.Select(i => new EpisodeExpression(i) }.Select(i => new EpisodeExpression(i)
{ {
IsNamed = true IsNamed = true

View File

@ -77,7 +77,7 @@ namespace Emby.Naming.TV
if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase)) if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{ {
var testFilename = filename.Substring(1); var testFilename = filename.AsSpan().Slice(1);
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{ {

View File

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

View File

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

View File

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -43,10 +42,10 @@ using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using Emby.Server.Implementations.SyncPlay; using Jellyfin.Api.Helpers;
using MediaBrowser.Api;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
@ -78,8 +77,8 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.TV;
using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
@ -97,7 +96,6 @@ using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.Subtitles;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -192,7 +190,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the application paths. /// Gets or sets the application paths.
/// </summary> /// </summary>
/// <value>The application paths.</value> /// <value>The application paths.</value>
protected ServerApplicationPaths ApplicationPaths { get; set; } protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
@ -236,7 +234,7 @@ namespace Emby.Server.Implementations
/// Initializes a new instance of the <see cref="ApplicationHost" /> class. /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// </summary> /// </summary>
public ApplicationHost( public ApplicationHost(
ServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IFileSystem fileSystem, IFileSystem fileSystem,
@ -484,12 +482,10 @@ namespace Emby.Server.Implementations
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
pluginBuilder.AppendLine( pluginBuilder.Append(plugin.Name)
string.Format( .Append(' ')
CultureInfo.InvariantCulture, .Append(plugin.Version)
"{0} {1}", .AppendLine();
plugin.Name,
plugin.Version));
} }
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
@ -556,8 +552,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@ -566,10 +560,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>)); serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
// TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>)); serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
serviceCollection.AddSingleton<IMediaEncoder>(provider => serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@ -638,6 +630,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<EncodingHelper>(); serviceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
serviceCollection.AddSingleton<TranscodingJobHelper>();
} }
/// <summary> /// <summary>
@ -654,7 +648,6 @@ namespace Emby.Server.Implementations
_httpServer = Resolve<IHttpServer>(); _httpServer = Resolve<IHttpServer>();
_httpClient = Resolve<IHttpClient>(); _httpClient = Resolve<IHttpClient>();
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties(); SetStaticProperties();
@ -799,7 +792,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>()); Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
} }
@ -873,6 +865,11 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
continue; continue;
} }
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
continue;
}
foreach (Type type in exportedTypes) foreach (Type type in exportedTypes)
{ {
@ -1034,12 +1031,6 @@ namespace Emby.Server.Implementations
} }
} }
// Include composable parts in the Api assembly
yield return typeof(ApiEntryPoint).Assembly;
// Include composable parts in the Dashboard assembly
yield return typeof(DashboardService).Assembly;
// Include composable parts in the Model assembly // Include composable parts in the Model assembly
yield return typeof(SystemInfo).Assembly; yield return typeof(SystemInfo).Assembly;
@ -1155,7 +1146,7 @@ namespace Emby.Server.Implementations
return null; return null;
} }
return GetLocalApiUrl(addresses.First()); return GetLocalApiUrl(addresses[0]);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1228,7 +1219,7 @@ namespace Emby.Server.Implementations
var addresses = ServerConfigurationManager var addresses = ServerConfigurationManager
.Configuration .Configuration
.LocalNetworkAddresses .LocalNetworkAddresses
.Select(NormalizeConfiguredLocalAddress) .Select(x => NormalizeConfiguredLocalAddress(x))
.Where(i => i != null) .Where(i => i != null)
.ToList(); .ToList();
@ -1249,8 +1240,7 @@ namespace Emby.Server.Implementations
} }
} }
var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
if (valid)
{ {
resultList.Add(address); resultList.Add(address);
@ -1264,13 +1254,12 @@ namespace Emby.Server.Implementations
return resultList; return resultList;
} }
public IPAddress NormalizeConfiguredLocalAddress(string address) public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
{ {
var index = address.Trim('/').IndexOf('/'); var index = address.Trim('/').IndexOf('/');
if (index != -1) if (index != -1)
{ {
address = address.Substring(index + 1); address = address.Slice(index + 1);
} }
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))

View File

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

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -7,6 +6,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie; using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
/// <summary> /// <summary>
@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
/// <param name="userDataManager">The user data manager.</param> /// <param name="userDataManager">The user data manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param> /// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param> /// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param>
public ChannelManager( public ChannelManager(
IUserManager userManager, IUserManager userManager,
IDtoService dtoService, IDtoService dtoService,
@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem, IFileSystem fileSystem,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IProviderManager providerManager) IProviderManager providerManager,
IMemoryCache memoryCache)
{ {
_userManager = userManager; _userManager = userManager;
_dtoService = dtoService; _dtoService = dtoService;
@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager; _userDataManager = userDataManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_providerManager = providerManager; _providerManager = providerManager;
_memoryCache = memoryCache;
} }
internal IChannel[] Channels { get; private set; } internal IChannel[] Channels { get; private set; }
@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken) private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
{ {
if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo)) if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
{ {
if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5) return cachedInfo;
{
return cachedInfo.Item2;
}
} }
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken) var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
var list = mediaInfo.ToList(); var list = mediaInfo.ToList();
_memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
_channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
return list; return list;
} }

View File

@ -363,60 +363,4 @@ namespace Emby.Server.Implementations.Collections
return results.Values; return results.Values;
} }
} }
/// <summary>
/// The collection manager entry point.
/// </summary>
public sealed class CollectionManagerEntryPoint : IServerEntryPoint
{
private readonly CollectionManager _collectionManager;
private readonly IServerConfigurationManager _config;
private readonly ILogger<CollectionManagerEntryPoint> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CollectionManagerEntryPoint"/> class.
/// </summary>
/// <param name="collectionManager">The collection manager.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="logger">The logger.</param>
public CollectionManagerEntryPoint(
ICollectionManager collectionManager,
IServerConfigurationManager config,
ILogger<CollectionManagerEntryPoint> logger)
{
_collectionManager = (CollectionManager)collectionManager;
_config = config;
_logger = logger;
}
/// <inheritdoc />
public async Task RunAsync()
{
if (!_config.Configuration.CollectionsUpgraded && _config.Configuration.IsStartupWizardCompleted)
{
var path = _collectionManager.GetCollectionsFolderPath();
if (Directory.Exists(path))
{
try
{
await _collectionManager.EnsureLibraryFolder(path, true).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating camera uploads library");
}
_config.Configuration.CollectionsUpgraded = true;
_config.SaveConfiguration();
}
}
}
/// <inheritdoc />
public void Dispose()
{
// Nothing to dispose
}
}
} }

View File

@ -109,7 +109,6 @@ namespace Emby.Server.Implementations.Configuration
if (!string.IsNullOrWhiteSpace(newPath) if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal)) && !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
{ {
// Validate
if (!File.Exists(newPath)) if (!File.Exists(newPath))
{ {
throw new FileNotFoundException( throw new FileNotFoundException(
@ -133,7 +132,6 @@ namespace Emby.Server.Implementations.Configuration
if (!string.IsNullOrWhiteSpace(newPath) if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal)) && !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal))
{ {
// Validate
if (!Directory.Exists(newPath)) if (!Directory.Exists(newPath))
{ {
throw new DirectoryNotFoundException( throw new DirectoryNotFoundException(
@ -146,60 +144,5 @@ namespace Emby.Server.Implementations.Configuration
EnsureWriteAccess(newPath); EnsureWriteAccess(newPath);
} }
} }
/// <summary>
/// Sets all configuration values to their optimal values.
/// </summary>
/// <returns>If the configuration changed.</returns>
public bool SetOptimalValues()
{
var config = Configuration;
var changed = false;
if (!config.EnableCaseSensitiveItemIds)
{
config.EnableCaseSensitiveItemIds = true;
changed = true;
}
if (!config.SkipDeserializationForBasicTypes)
{
config.SkipDeserializationForBasicTypes = true;
changed = true;
}
if (!config.EnableSimpleArtistDetection)
{
config.EnableSimpleArtistDetection = true;
changed = true;
}
if (!config.EnableNormalizedItemByNameIds)
{
config.EnableNormalizedItemByNameIds = true;
changed = true;
}
if (!config.DisableLiveTvChannelUserDataName)
{
config.DisableLiveTvChannelUserDataName = true;
changed = true;
}
if (!config.EnableNewOmdbSupport)
{
config.EnableNewOmdbSupport = true;
changed = true;
}
if (!config.CollectionsUpgraded)
{
config.CollectionsUpgraded = true;
changed = true;
}
return changed;
}
} }
} }

View File

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Updates;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations namespace Emby.Server.Implementations
@ -19,7 +18,8 @@ namespace Emby.Server.Implementations
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" }, { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" }, { FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" }, { FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString } { PlaylistsAllowDuplicatesKey, bool.TrueString },
{ BindToUnixSocketKey, bool.FalseString }
}; };
} }
} }

View File

@ -1,225 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
/// <summary>
/// Class SQLiteDisplayPreferencesRepository.
/// </summary>
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
{
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonOptions;
public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
: base(logger)
{
_fileSystem = fileSystem;
_jsonOptions = JsonDefaults.GetOptions();
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
}
/// <summary>
/// Gets the name of the repository.
/// </summary>
/// <value>The name.</value>
public string Name => "SQLite";
public void Initialize()
{
try
{
InitializeInternal();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
_fileSystem.DeleteFile(DbFilePath);
InitializeInternal();
}
}
/// <summary>
/// Opens the connection to the database.
/// </summary>
/// <returns>Task.</returns>
private void InitializeInternal()
{
string[] queries =
{
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
"create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
};
using (var connection = GetConnection())
{
connection.RunQueries(queries);
}
}
/// <summary>
/// Save the display preferences associated with an item in the repo.
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
{
if (displayPreferences == null)
{
throw new ArgumentNullException(nameof(displayPreferences));
}
if (string.IsNullOrEmpty(displayPreferences.Id))
{
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
}
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
{
connection.RunInTransaction(
db => SaveDisplayPreferences(displayPreferences, userId, client, db),
TransactionMode);
}
}
private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
{
var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
{
statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
statement.TryBind("@userId", userId.ToByteArray());
statement.TryBind("@client", client);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}
/// <summary>
/// Save all display preferences associated with a user in the repo.
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
{
if (displayPreferences == null)
{
throw new ArgumentNullException(nameof(displayPreferences));
}
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
foreach (var displayPreference in displayPreferences)
{
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
}
},
TransactionMode);
}
}
/// <summary>
/// Gets the display preferences.
/// </summary>
/// <param name="displayPreferencesId">The display preferences id.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <returns>Task{DisplayPreferences}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
{
if (string.IsNullOrEmpty(displayPreferencesId))
{
throw new ArgumentNullException(nameof(displayPreferencesId));
}
var guidId = displayPreferencesId.GetMD5();
using (var connection = GetConnection(true))
{
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
{
statement.TryBind("@id", guidId.ToByteArray());
statement.TryBind("@userId", userId.ToByteArray());
statement.TryBind("@client", client);
foreach (var row in statement.ExecuteQuery())
{
return Get(row);
}
}
}
return new DisplayPreferences
{
Id = guidId.ToString("N", CultureInfo.InvariantCulture)
};
}
/// <summary>
/// Gets all display preferences for the given user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>Task{DisplayPreferences}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
{
var list = new List<DisplayPreferences>();
using (var connection = GetConnection(true))
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
{
statement.TryBind("@userId", userId.ToByteArray());
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
}
return list;
}
private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
=> JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
=> SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
=> GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
}
}

View File

@ -9,6 +9,7 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
"OwnerId" "OwnerId"
}; };
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
private static readonly string[] _mediaStreamSaveColumns = private static readonly string[] _mediaStreamSaveColumns =
{ {
"ItemId", "ItemId",
@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
"ColorTransfer" "ColorTransfer"
}; };
private static readonly string _mediaStreamSaveColumnsInsertQuery =
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
private static readonly string _mediaStreamSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
private static readonly string[] _mediaAttachmentSaveColumns = private static readonly string[] _mediaAttachmentSaveColumns =
{ {
"ItemId", "ItemId",
@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
"MIMEType" "MIMEType"
}; };
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
private static readonly string _mediaAttachmentInsertPrefix; private static readonly string _mediaAttachmentInsertPrefix;
private static string GetSaveItemCommandText() private const string SaveItemCommandText =
{ @"replace into TypedBaseItems
var saveColumns = new[] (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
{ values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
"guid",
"type",
"data",
"Path",
"StartDate",
"EndDate",
"ChannelId",
"IsMovie",
"IsSeries",
"EpisodeTitle",
"IsRepeat",
"CommunityRating",
"CustomRating",
"IndexNumber",
"IsLocked",
"Name",
"OfficialRating",
"MediaType",
"Overview",
"ParentIndexNumber",
"PremiereDate",
"ProductionYear",
"ParentId",
"Genres",
"InheritedParentalRatingValue",
"SortName",
"ForcedSortName",
"RunTimeTicks",
"Size",
"DateCreated",
"DateModified",
"PreferredMetadataLanguage",
"PreferredMetadataCountryCode",
"Width",
"Height",
"DateLastRefreshed",
"DateLastSaved",
"IsInMixedFolder",
"LockedFields",
"Studios",
"Audio",
"ExternalServiceId",
"Tags",
"IsFolder",
"UnratedType",
"TopParentId",
"TrailerTypes",
"CriticRating",
"CleanName",
"PresentationUniqueKey",
"OriginalTitle",
"PrimaryVersionId",
"DateLastMediaAdded",
"Album",
"IsVirtualItem",
"SeriesName",
"UserDataKey",
"SeasonName",
"SeasonId",
"SeriesId",
"ExternalSeriesId",
"Tagline",
"ProviderIds",
"Images",
"ProductionLocations",
"ExtraIds",
"TotalBitrate",
"ExtraType",
"Artists",
"AlbumArtists",
"ExternalId",
"SeriesPresentationUniqueKey",
"ShowId",
"OwnerId"
};
var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
for (var i = 0; i < saveColumns.Length; i++)
{
if (i != 0)
{
saveItemCommandCommandText += ",";
}
saveItemCommandCommandText += "@" + saveColumns[i];
}
return saveItemCommandCommandText + ")";
}
/// <summary> /// <summary>
/// Save a standard item in the repo. /// Save a standard item in the repo.
@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
{ {
var statements = PrepareAll(db, new string[] var statements = PrepareAll(db, new string[]
{ {
GetSaveItemCommandText(), SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId" "delete from AncestorIds where ItemId=@ItemId"
}).ToList(); }).ToList();
@ -1056,7 +978,10 @@ namespace Emby.Server.Implementations.Data
continue; continue;
} }
str.Append($"{i.Key}={i.Value}|"); str.Append(i.Key)
.Append('=')
.Append(i.Value)
.Append('|');
} }
if (str.Length == 0) if (str.Length == 0)
@ -1110,7 +1035,8 @@ namespace Emby.Server.Implementations.Data
continue; continue;
} }
str.Append(ToValueString(i) + "|"); AppendItemImageInfo(str, i);
str.Append('|');
} }
str.Length -= 1; // Remove last | str.Length -= 1; // Remove last |
@ -1144,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
item.ImageInfos = list.ToArray(); item.ImageInfos = list.ToArray();
} }
public string ToValueString(ItemImageInfo image) public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
{ {
const string Delimeter = "*"; const char Delimiter = '*';
var path = image.Path ?? string.Empty; var path = image.Path ?? string.Empty;
var hash = image.BlurHash ?? string.Empty; var hash = image.BlurHash ?? string.Empty;
return GetPathToSave(path) + bldr.Append(GetPathToSave(path))
Delimeter + .Append(Delimiter)
image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + .Append(image.DateModified.Ticks)
Delimeter + .Append(Delimiter)
image.Type + .Append(image.Type)
Delimeter + .Append(Delimiter)
image.Width.ToString(CultureInfo.InvariantCulture) + .Append(image.Width)
Delimeter + .Append(Delimiter)
image.Height.ToString(CultureInfo.InvariantCulture) + .Append(image.Height)
Delimeter + .Append(Delimiter)
// Replace delimiters with other characters. // Replace delimiters with other characters.
// This can be removed when we migrate to a proper DB. // This can be removed when we migrate to a proper DB.
hash.Replace('*', '/').Replace('|', '\\'); .Append(hash.Replace('*', '/').Replace('|', '\\'));
} }
public ItemImageInfo ItemImageInfoFromValueString(string value) public ItemImageInfo ItemImageInfoFromValueString(string value)
@ -1225,7 +1151,7 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection(true)) using (var connection = GetConnection(true))
{ {
using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid")) using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
{ {
statement.TryBind("@guid", id); statement.TryBind("@guid", id);
@ -2471,7 +2397,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo; var item = query.SimilarTo;
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("("); builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating)) if (string.IsNullOrEmpty(item.OfficialRating))
{ {
@ -2509,7 +2435,7 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(query.SearchTerm)) if (!string.IsNullOrEmpty(query.SearchTerm))
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("("); builder.Append('(');
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
@ -2775,82 +2701,82 @@ namespace Emby.Server.Implementations.Data
private string FixUnicodeChars(string buffer) private string FixUnicodeChars(string buffer)
{ {
if (buffer.IndexOf('\u2013') > -1) if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2013', '-'); // en dash buffer = buffer.Replace('\u2013', '-'); // en dash
} }
if (buffer.IndexOf('\u2014') > -1) if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2014', '-'); // em dash buffer = buffer.Replace('\u2014', '-'); // em dash
} }
if (buffer.IndexOf('\u2015') > -1) if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2015', '-'); // horizontal bar buffer = buffer.Replace('\u2015', '-'); // horizontal bar
} }
if (buffer.IndexOf('\u2017') > -1) if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2017', '_'); // double low line buffer = buffer.Replace('\u2017', '_'); // double low line
} }
if (buffer.IndexOf('\u2018') > -1) if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
} }
if (buffer.IndexOf('\u2019') > -1) if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
} }
if (buffer.IndexOf('\u201a') > -1) if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
} }
if (buffer.IndexOf('\u201b') > -1) if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
} }
if (buffer.IndexOf('\u201c') > -1) if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
} }
if (buffer.IndexOf('\u201d') > -1) if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
} }
if (buffer.IndexOf('\u201e') > -1) if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
} }
if (buffer.IndexOf('\u2026') > -1) if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
} }
if (buffer.IndexOf('\u2032') > -1) if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2032', '\''); // prime buffer = buffer.Replace('\u2032', '\''); // prime
} }
if (buffer.IndexOf('\u2033') > -1) if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2033', '\"'); // double prime buffer = buffer.Replace('\u2033', '\"'); // double prime
} }
if (buffer.IndexOf('\u0060') > -1) if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u0060', '\''); // grave accent buffer = buffer.Replace('\u0060', '\''); // grave accent
} }
if (buffer.IndexOf('\u00B4') > -1) if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u00B4', '\''); // acute accent buffer = buffer.Replace('\u00B4', '\''); // acute accent
} }
@ -2999,7 +2925,6 @@ namespace Emby.Server.Implementations.Data
{ {
connection.RunInTransaction(db => connection.RunInTransaction(db =>
{ {
var statements = PrepareAll(db, statementTexts).ToList(); var statements = PrepareAll(db, statementTexts).ToList();
if (!isReturningZeroItems) if (!isReturningZeroItems)
@ -4669,8 +4594,12 @@ namespace Emby.Server.Implementations.Data
if (query.BlockUnratedItems.Length > 1) if (query.BlockUnratedItems.Length > 1)
{ {
var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'")); var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause)); whereClauses.Add(
string.Format(
CultureInfo.InvariantCulture,
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
inClause));
} }
if (query.ExcludeInheritedTags.Length > 0) if (query.ExcludeInheritedTags.Length > 0)
@ -4679,7 +4608,7 @@ namespace Emby.Server.Implementations.Data
if (statement == null) if (statement == null)
{ {
int index = 0; int index = 0;
string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++)); string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)"); whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
} }
else else
@ -5238,7 +5167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{ {
if (i > 0) if (i > 0)
{ {
insertText.Append(","); insertText.Append(',');
} }
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture)); insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
@ -5733,10 +5662,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 100; const int Limit = 100;
var startIndex = 0; var startIndex = 0;
const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
var insertText = new StringBuilder(StartInsertText);
while (startIndex < values.Count) while (startIndex < values.Count)
{ {
var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
var endIndex = Math.Min(values.Count, startIndex + Limit); var endIndex = Math.Min(values.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++) for (var i = startIndex; i < endIndex; i++)
@ -5778,6 +5707,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
} }
startIndex += Limit; startIndex += Limit;
insertText.Length = StartInsertText.Length;
} }
} }
@ -5815,10 +5745,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var startIndex = 0; var startIndex = 0;
var listIndex = 0; var listIndex = 0;
const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
var insertText = new StringBuilder(StartInsertText);
while (startIndex < people.Count) while (startIndex < people.Count)
{ {
var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
var endIndex = Math.Min(people.Count, startIndex + Limit); var endIndex = Math.Min(people.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++) for (var i = startIndex; i < endIndex; i++)
{ {
@ -5852,6 +5782,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
} }
startIndex += Limit; startIndex += Limit;
insertText.Length = StartInsertText.Length;
} }
} }
@ -5890,10 +5821,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query)); throw new ArgumentNullException(nameof(query));
} }
var cmdText = "select " var cmdText = _mediaStreamSaveColumnsSelectQuery;
+ string.Join(",", _mediaStreamSaveColumns)
+ " from mediastreams where"
+ " ItemId=@ItemId";
if (query.Type.HasValue) if (query.Type.HasValue)
{ {
@ -5970,18 +5898,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 10; const int Limit = 10;
var startIndex = 0; var startIndex = 0;
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
while (startIndex < streams.Count) while (startIndex < streams.Count)
{ {
var insertText = new StringBuilder("insert into mediastreams (");
foreach (var column in _mediaStreamSaveColumns)
{
insertText.Append(column).Append(',');
}
// Remove last comma
insertText.Length--;
insertText.Append(") values ");
var endIndex = Math.Min(streams.Count, startIndex + Limit); var endIndex = Math.Min(streams.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++) for (var i = startIndex; i < endIndex; i++)
@ -6064,6 +5983,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
} }
startIndex += Limit; startIndex += Limit;
insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
} }
} }
@ -6247,10 +6167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query)); throw new ArgumentNullException(nameof(query));
} }
var cmdText = "select " var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
+ string.Join(",", _mediaAttachmentSaveColumns)
+ " from mediaattachments where"
+ " ItemId=@ItemId";
if (query.Index.HasValue) if (query.Index.HasValue)
{ {
@ -6318,10 +6235,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{ {
const int InsertAtOnce = 10; const int InsertAtOnce = 10;
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce) for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
{ {
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce); var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
for (var i = startIndex; i < endIndex; i++) for (var i = startIndex; i < endIndex; i++)
@ -6331,7 +6247,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{ {
insertText.Append("@" + column + index + ","); insertText.Append('@')
.Append(column)
.Append(index)
.Append(',');
} }
insertText.Length -= 1; insertText.Length -= 1;
@ -6364,6 +6283,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statement.Reset(); statement.Reset();
statement.MoveNext(); statement.MoveNext();
} }
insertText.Length = _mediaAttachmentInsertPrefix.Length;
} }
} }

View File

@ -5,8 +5,8 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices namespace Emby.Server.Implementations.Devices
{ {
public class DeviceManager : IDeviceManager public class DeviceManager : IDeviceManager
{ {
private readonly IMemoryCache _memoryCache;
private readonly IJsonSerializer _json; private readonly IJsonSerializer _json;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo; private readonly IAuthenticationRepository _authRepo;
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
private readonly object _capabilitiesSyncLock = new object(); private readonly object _capabilitiesSyncLock = new object();
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
IAuthenticationRepository authRepo, IAuthenticationRepository authRepo,
IJsonSerializer json, IJsonSerializer json,
IUserManager userManager, IUserManager userManager,
IServerConfigurationManager config) IServerConfigurationManager config,
IMemoryCache memoryCache)
{ {
_json = json; _json = json;
_userManager = userManager; _userManager = userManager;
_config = config; _config = config;
_memoryCache = memoryCache;
_authRepo = authRepo; _authRepo = authRepo;
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
} }
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
lock (_capabilitiesSyncLock) lock (_capabilitiesSyncLock)
{ {
_capabilitiesCache[deviceId] = capabilities; _memoryCache.Set(deviceId, capabilities);
_json.SerializeToFile(capabilities, path); _json.SerializeToFile(capabilities, path);
} }
} }
@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id) public ClientCapabilities GetCapabilities(string id)
{ {
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
{
return result;
}
lock (_capabilitiesSyncLock) lock (_capabilitiesSyncLock)
{ {
if (_capabilitiesCache.TryGetValue(id, out var result))
{
return result;
}
var path = Path.Combine(GetDevicePath(id), "capabilities.json"); var path = Path.Combine(GetDevicePath(id), "capabilities.json");
try try
{ {

View File

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

View File

@ -13,10 +13,8 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
<ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" /> <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
@ -25,7 +23,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.5.211" /> <PackageReference Include="IPNetwork2" Version="2.5.211" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
@ -34,14 +32,14 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" 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.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
<PackageReference Include="Mono.Nat" Version="2.0.1" /> <PackageReference Include="Mono.Nat" Version="2.0.2" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
<PackageReference Include="sharpcompress" Version="0.25.1" /> <PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.0.9" /> <PackageReference Include="DotNet.Glob" Version="3.0.9" />
</ItemGroup> </ItemGroup>
@ -54,7 +52,7 @@
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->

View File

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

View File

@ -1,3 +1,4 @@
using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Udp; using Emby.Server.Implementations.Udp;
@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc /> /// <inheritdoc />
public Task RunAsync() public Task RunAsync()
{ {
_udpServer = new UdpServer(_logger, _appHost, _config); try
_udpServer.Start(PortNumber, _cancellationTokenSource.Token); {
_udpServer = new UdpServer(_logger, _appHost, _config);
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
}
catch (SocketException ex)
{
_logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
}
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
private readonly IStreamHelper _streamHelper; private readonly IStreamHelper _streamHelper;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
/// <summary> /// <summary>
/// The _options. /// The _options.
@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
} }
_streamHelper = streamHelper; _streamHelper = streamHelper;
_fileSystem = fileSystem;
Path = path; Path = path;
_logger = logger; _logger = logger;

View File

@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{ {
httpRes.StatusCode = 200; httpRes.StatusCode = 200;
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq)) foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
{ {
httpRes.Headers.Add(key, value); httpRes.Headers.Add(key, value);
} }
@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
var handler = GetServiceHandler(httpReq); var handler = GetServiceHandler(httpReq);
if (handler != null) if (handler != null)
{ {
await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false); await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
} }
else else
{ {
@ -567,7 +567,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
var connection = new WebSocketConnection( using var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(), _loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket, webSocket,
context.Connection.RemoteIpAddress, context.Connection.RemoteIpAddress,

View File

@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>(); responseHeaders = new Dictionary<string, string>();
} }
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires)) if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
{ {
responseHeaders[HeaderNames.Expires] = "0"; responseHeaders[HeaderNames.Expires] = "0";
} }
@ -326,7 +326,8 @@ namespace Emby.Server.Implementations.HttpServer
return GetHttpResult(request, ms, contentType, true, responseHeaders); return GetHttpResult(request, ms, contentType, true, responseHeaders);
} }
private IHasHeaders GetCompressedResult(byte[] content, private IHasHeaders GetCompressedResult(
byte[] content,
string requestedCompressionType, string requestedCompressionType,
IDictionary<string, string> responseHeaders, IDictionary<string, string> responseHeaders,
bool isHeadRequest, bool isHeadRequest,

View File

@ -13,35 +13,31 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer.Security namespace Emby.Server.Implementations.HttpServer.Security
{ {
public class AuthService : IAuthService public class AuthService : IAuthService
{ {
private readonly ILogger<AuthService> _logger;
private readonly IAuthorizationContext _authorizationContext; private readonly IAuthorizationContext _authorizationContext;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
public AuthService( public AuthService(
ILogger<AuthService> logger,
IAuthorizationContext authorizationContext, IAuthorizationContext authorizationContext,
IServerConfigurationManager config, IServerConfigurationManager config,
ISessionManager sessionManager, ISessionManager sessionManager,
INetworkManager networkManager) INetworkManager networkManager)
{ {
_logger = logger;
_authorizationContext = authorizationContext; _authorizationContext = authorizationContext;
_config = config; _config = config;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_networkManager = networkManager; _networkManager = networkManager;
} }
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues) public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{ {
ValidateUser(request, authAttribtues); ValidateUser(request, authAttributes);
} }
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@ -67,17 +63,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
return auth; return auth;
} }
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues) private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{ {
// This code is executed before the service // This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request); var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(authAttribtues, request)) if (!IsExemptFromAuthenticationToken(authAttributes, request))
{ {
ValidateSecurityToken(request, auth.Token); ValidateSecurityToken(request, auth.Token);
} }
if (authAttribtues.AllowLocalOnly && !request.IsLocal) if (authAttributes.AllowLocalOnly && !request.IsLocal)
{ {
throw new SecurityException("Operation not found."); throw new SecurityException("Operation not found.");
} }
@ -91,14 +87,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user != null) if (user != null)
{ {
ValidateUserAccess(user, request, authAttribtues, auth); ValidateUserAccess(user, request, authAttributes);
} }
var info = GetTokenInfo(request); var info = GetTokenInfo(request);
if (!IsExemptFromRoles(auth, authAttribtues, request, info)) if (!IsExemptFromRoles(auth, authAttributes, request, info))
{ {
var roles = authAttribtues.GetRoles(); var roles = authAttributes.GetRoles();
ValidateRoles(roles, user); ValidateRoles(roles, user);
} }
@ -122,8 +118,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess( private void ValidateUserAccess(
User user, User user,
IRequest request, IRequest request,
IAuthenticationAttributes authAttributes, IAuthenticationAttributes authAttributes)
AuthorizationInfo auth)
{ {
if (user.HasPermission(PermissionKind.IsDisabled)) if (user.HasPermission(PermissionKind.IsDisabled))
{ {
@ -162,6 +157,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
return true; return true;
} }
if (authAttribtues.IgnoreLegacyAuth)
{
return true;
}
return false; return false;
} }
@ -241,16 +241,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
{ {
throw new AuthenticationException("Access token is invalid or expired."); throw new AuthenticationException("Access token is invalid or expired.");
} }
// if (!string.IsNullOrEmpty(info.UserId))
//{
// var user = _userManager.GetUserById(info.UserId);
// if (user == null || user.Configuration.IsDisabled)
// {
// throw new SecurityException("User account has been disabled.");
// }
//}
} }
} }
} }

View File

@ -97,6 +97,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
token = headers["X-MediaBrowser-Token"]; token = headers["X-MediaBrowser-Token"];
} }
if (string.IsNullOrEmpty(token))
{
token = queryString["ApiKey"];
}
// TODO deprecate this query parameter.
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
token = queryString["api_key"]; token = queryString["api_key"];
@ -276,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private static string NormalizeValue(string value) private static string NormalizeValue(string value)
{ {
if (string.IsNullOrEmpty(value)) return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
{
return value;
}
return WebUtility.HtmlEncode(value);
} }
} }
} }

View File

@ -95,13 +95,13 @@ namespace Emby.Server.Implementations.HttpServer
if (bytes != null) if (bytes != null)
{ {
await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
} }
else else
{ {
using (var src = SourceStream) using (var src = SourceStream)
{ {
await src.CopyToAsync(responseStream).ConfigureAwait(false); await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary> /// <summary>
/// Class WebSocketConnection. /// Class WebSocketConnection.
/// </summary> /// </summary>
public class WebSocketConnection : IWebSocketConnection public class WebSocketConnection : IWebSocketConnection, IDisposable
{ {
/// <summary> /// <summary>
/// The logger. /// The logger.
@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.HttpServer
Memory<byte> memory = writer.GetMemory(512); Memory<byte> memory = writer.GetMemory(512);
try try
{ {
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken); receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
} }
catch (WebSocketException ex) catch (WebSocketException ex)
{ {
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
writer.Advance(bytesRead); writer.Advance(bytesRead);
// Make the data available to the PipeReader // Make the data available to the PipeReader
FlushResult flushResult = await writer.FlushAsync(); FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
if (flushResult.IsCompleted) if (flushResult.IsCompleted)
{ {
// The PipeReader stopped reading // The PipeReader stopped reading
@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal)) if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
{ {
await SendKeepAliveResponse(); await SendKeepAliveResponse().ConfigureAwait(false);
} }
else else
{ {

View File

@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<string> _affectedPaths = new List<string>(); private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object(); private readonly object _timerLock = new object();
private Timer _timer; private Timer _timer;
private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger) public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
{ {
@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
} }
} }
private bool _disposed;
public void Dispose() public void Dispose()
{ {
_disposed = true; _disposed = true;
DisposeTimer(); DisposeTimer();
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
if (info is FileInfo fileInfo) if (info is FileInfo fileInfo)
{ {
result.Length = fileInfo.Length; result.Length = fileInfo.Length;
// Issue #2354 get the size of files behind symbolic links
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
{
result.Length = thisFileStream.Length;
}
}
result.DirectoryName = fileInfo.DirectoryName; result.DirectoryName = fileInfo.DirectoryName;
} }

View File

@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
/// </summary> /// </summary>
public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist> public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
{ {
/// <summary> public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
/// The library manager. : base(fileSystem, providerManager, applicationPaths, imageProcessor)
/// </summary>
private readonly ILibraryManager _libraryManager;
public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{ {
_libraryManager = libraryManager;
} }
/// <summary> /// <summary>

View File

@ -3,7 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Emby.Server.Implementations.Images; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@ -1,7 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Server.Implementations.Images; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
@ -13,19 +14,28 @@ namespace Emby.Server.Implementations.Library
public class CoreResolutionIgnoreRule : IResolverIgnoreRule public class CoreResolutionIgnoreRule : IResolverIgnoreRule
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IServerApplicationPaths _serverApplicationPaths;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class. /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
/// </summary> /// </summary>
/// <param name="libraryManager">The library manager.</param> /// <param name="libraryManager">The library manager.</param>
public CoreResolutionIgnoreRule(ILibraryManager libraryManager) /// <param name="serverApplicationPaths">The server application paths.</param>
public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_serverApplicationPaths = serverApplicationPaths;
} }
/// <inheritdoc /> /// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent) public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
{ {
// Don't ignore application folders
if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))
{
return false;
}
// Don't ignore top level folders // Don't ignore top level folders
if (fileInfo.IsDirectory && parent is AggregateFolder) if (fileInfo.IsDirectory && parent is AggregateFolder)
{ {
@ -67,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null) if (parent != null)
{ {
// Don't resolve these into audio files // Don't resolve these into audio files
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename) if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename)) && _libraryManager.IsAudioFile(filename))
{ {
return true; return true;

View File

@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
{ {
public class ExclusiveLiveStream : ILiveStream public class ExclusiveLiveStream : ILiveStream
{ {
private readonly Func<Task> _closeFn;
public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
{
MediaSource = mediaSource;
EnableStreamSharing = false;
_closeFn = closeFn;
ConsumerCount = 1;
UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public int ConsumerCount { get; set; } public int ConsumerCount { get; set; }
public string OriginalStreamId { get; set; } public string OriginalStreamId { get; set; }
@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
public MediaSourceInfo MediaSource { get; set; } public MediaSourceInfo MediaSource { get; set; }
public string UniqueId { get; private set; } public string UniqueId { get; }
private Func<Task> _closeFn;
public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
{
MediaSource = mediaSource;
EnableStreamSharing = false;
_closeFn = closeFn;
ConsumerCount = 1;
UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public Task Close() public Task Close()
{ {

View File

@ -1,3 +1,6 @@
#nullable enable
using System;
using System.Linq; using System.Linq;
using DotNet.Globbing; using DotNet.Globbing;
@ -11,40 +14,76 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Files matching these glob patterns will be ignored. /// Files matching these glob patterns will be ignored.
/// </summary> /// </summary>
public static readonly string[] Patterns = new string[] private static readonly string[] _patterns =
{ {
"**/small.jpg", "**/small.jpg",
"**/albumart.jpg", "**/albumart.jpg",
"**/*sample*",
// We have neither non-greedy matching or character group repetitions, working around that here.
// https://github.com/dazinator/DotNet.Glob#patterns
// .*/sample\..{1,5}
"**/sample.?",
"**/sample.??",
"**/sample.???", // Matches sample.mkv
"**/sample.????", // Matches sample.webm
"**/sample.?????",
"**/*.sample.?",
"**/*.sample.??",
"**/*.sample.???",
"**/*.sample.????",
"**/*.sample.?????",
"**/sample/*",
// Directories // Directories
"**/metadata/**", "**/metadata/**",
"**/metadata",
"**/ps3_update/**", "**/ps3_update/**",
"**/ps3_update",
"**/ps3_vprm/**", "**/ps3_vprm/**",
"**/ps3_vprm",
"**/extrafanart/**", "**/extrafanart/**",
"**/extrafanart",
"**/extrathumbs/**", "**/extrathumbs/**",
"**/extrathumbs",
"**/.actors/**", "**/.actors/**",
"**/.actors",
"**/.wd_tv/**", "**/.wd_tv/**",
"**/.wd_tv",
"**/lost+found/**", "**/lost+found/**",
"**/lost+found",
// WMC temp recording directories that will constantly be written to // WMC temp recording directories that will constantly be written to
"**/TempRec/**", "**/TempRec/**",
"**/TempRec",
"**/TempSBE/**", "**/TempSBE/**",
"**/TempSBE",
// Synology // Synology
"**/eaDir/**", "**/eaDir/**",
"**/eaDir",
"**/@eaDir/**", "**/@eaDir/**",
"**/@eaDir",
"**/#recycle/**", "**/#recycle/**",
"**/#recycle",
// Qnap // Qnap
"**/@Recycle/**", "**/@Recycle/**",
"**/@Recycle",
"**/.@__thumb/**", "**/.@__thumb/**",
"**/.@__thumb",
"**/$RECYCLE.BIN/**", "**/$RECYCLE.BIN/**",
"**/$RECYCLE.BIN",
"**/System Volume Information/**", "**/System Volume Information/**",
"**/System Volume Information",
"**/.grab/**", "**/.grab/**",
"**/.grab",
// Unix hidden files and directories // Unix hidden files
"**/.*/**", "**/.*",
// Mac - if you ever remove the above.
// "**/._*",
// "**/.DS_Store",
// thumbs.db // thumbs.db
"**/thumbs.db", "**/thumbs.db",
@ -56,19 +95,31 @@ namespace Emby.Server.Implementations.Library
private static readonly GlobOptions _globOptions = new GlobOptions private static readonly GlobOptions _globOptions = new GlobOptions
{ {
Evaluation = { Evaluation =
{
CaseInsensitive = true CaseInsensitive = true
} }
}; };
private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray(); private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
/// <summary> /// <summary>
/// Returns true if the supplied path should be ignored. /// Returns true if the supplied path should be ignored.
/// </summary> /// </summary>
public static bool ShouldIgnore(string path) /// <param name="path">The path to test.</param>
/// <returns>Whether to ignore the path.</returns>
public static bool ShouldIgnore(ReadOnlySpan<char> path)
{ {
return _globs.Any(g => g.IsMatch(path)); int len = _globs.Length;
for (int i = 0; i < len; i++)
{
if (_globs[i].IsMatch(path))
{
return true;
}
}
return false;
} }
} }
} }

View File

@ -1,7 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo; using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre; using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person; using Person = MediaBrowser.Controller.Entities.Person;
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
using VideoResolver = Emby.Naming.Video.VideoResolver; using VideoResolver = Emby.Naming.Video.VideoResolver;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
@ -60,7 +59,10 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger; private readonly ILogger<LibraryManager> _logger;
private readonly IMemoryCache _memoryCache;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository; private readonly IUserDataManager _userDataRepository;
@ -72,12 +74,118 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository; private readonly IItemRepository _itemRepository;
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
/// <summary>
/// The _root folder sync lock.
/// </summary>
private readonly object _rootFolderSyncLock = new object();
private readonly object _userRootFolderSyncLock = new object();
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
private NamingOptions _namingOptions; private NamingOptions _namingOptions;
private string[] _videoFileExtensions; private string[] _videoFileExtensions;
/// <summary>
/// The _root folder.
/// </summary>
private volatile AggregateFolder _rootFolder;
private volatile UserRootFolder _userRootFolder;
private bool _wizardCompleted;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
/// </summary>
/// <param name="appHost">The application host.</param>
/// <param name="logger">The logger.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="userDataRepository">The user data repository.</param>
/// <param name="libraryMonitorFactory">The library monitor.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="providerManagerFactory">The provider manager.</param>
/// <param name="userviewManagerFactory">The userview manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILogger<LibraryManager> logger,
ITaskManager taskManager,
IUserManager userManager,
IServerConfigurationManager configurationManager,
IUserDataManager userDataRepository,
Lazy<ILibraryMonitor> libraryMonitorFactory,
IFileSystem fileSystem,
Lazy<IProviderManager> providerManagerFactory,
Lazy<IUserViewManager> userviewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
IMemoryCache memoryCache)
{
_appHost = appHost;
_logger = logger;
_taskManager = taskManager;
_userManager = userManager;
_configurationManager = configurationManager;
_userDataRepository = userDataRepository;
_libraryMonitorFactory = libraryMonitorFactory;
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
_userviewManagerFactory = userviewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
_memoryCache = memoryCache;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
RecordConfigurationValues(configurationManager.Configuration);
}
/// <summary>
/// Occurs when [item added].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <summary>
/// Occurs when [item updated].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary>
/// Occurs when [item removed].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
/// <summary>
/// Gets the root folder.
/// </summary>
/// <value>The root folder.</value>
public AggregateFolder RootFolder
{
get
{
if (_rootFolder == null)
{
lock (_rootFolderSyncLock)
{
if (_rootFolder == null)
{
_rootFolder = CreateRootFolder();
}
}
}
return _rootFolder;
}
}
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value; private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
private IProviderManager ProviderManager => _providerManagerFactory.Value; private IProviderManager ProviderManager => _providerManagerFactory.Value;
@ -116,75 +224,8 @@ namespace Emby.Server.Implementations.Library
/// <value>The comparers.</value> /// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; } private IBaseItemComparer[] Comparers { get; set; }
/// <summary>
/// Occurs when [item added].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <summary>
/// Occurs when [item updated].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary>
/// Occurs when [item removed].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
public bool IsScanRunning { get; private set; } public bool IsScanRunning { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
/// </summary>
/// <param name="appHost">The application host.</param>
/// <param name="logger">The logger.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="userDataRepository">The user data repository.</param>
/// <param name="libraryMonitorFactory">The library monitor.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="providerManagerFactory">The provider manager.</param>
/// <param name="userviewManagerFactory">The userview manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILogger<LibraryManager> logger,
ITaskManager taskManager,
IUserManager userManager,
IServerConfigurationManager configurationManager,
IUserDataManager userDataRepository,
Lazy<ILibraryMonitor> libraryMonitorFactory,
IFileSystem fileSystem,
Lazy<IProviderManager> providerManagerFactory,
Lazy<IUserViewManager> userviewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor)
{
_appHost = appHost;
_logger = logger;
_taskManager = taskManager;
_userManager = userManager;
_configurationManager = configurationManager;
_userDataRepository = userDataRepository;
_libraryMonitorFactory = libraryMonitorFactory;
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
_userviewManagerFactory = userviewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
_libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
RecordConfigurationValues(configurationManager.Configuration);
}
/// <summary> /// <summary>
/// Adds the parts. /// Adds the parts.
/// </summary> /// </summary>
@ -208,41 +249,6 @@ namespace Emby.Server.Implementations.Library
PostscanTasks = postscanTasks.ToArray(); PostscanTasks = postscanTasks.ToArray();
} }
/// <summary>
/// The _root folder.
/// </summary>
private volatile AggregateFolder _rootFolder;
/// <summary>
/// The _root folder sync lock.
/// </summary>
private readonly object _rootFolderSyncLock = new object();
/// <summary>
/// Gets the root folder.
/// </summary>
/// <value>The root folder.</value>
public AggregateFolder RootFolder
{
get
{
if (_rootFolder == null)
{
lock (_rootFolderSyncLock)
{
if (_rootFolder == null)
{
_rootFolder = CreateRootFolder();
}
}
}
return _rootFolder;
}
}
private bool _wizardCompleted;
/// <summary> /// <summary>
/// Records the configuration values. /// Records the configuration values.
/// </summary> /// </summary>
@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
_libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); _memoryCache.Set(item.Id, item);
} }
public void DeleteItem(BaseItem item, DeleteOptions options) public void DeleteItem(BaseItem item, DeleteOptions options)
@ -341,7 +347,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram) if (item is LiveTvProgram)
{ {
_logger.LogDebug( _logger.LogDebug(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name, item.GetType().Name,
item.Name ?? "Unknown name", item.Name ?? "Unknown name",
item.Path ?? string.Empty, item.Path ?? string.Empty,
@ -350,7 +356,7 @@ namespace Emby.Server.Implementations.Library
else else
{ {
_logger.LogInformation( _logger.LogInformation(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name, item.GetType().Name,
item.Name ?? "Unknown name", item.Name ?? "Unknown name",
item.Path ?? string.Empty, item.Path ?? string.Empty,
@ -368,7 +374,12 @@ namespace Emby.Server.Implementations.Library
continue; continue;
} }
_logger.LogDebug("Deleting path {MetadataPath}", metadataPath); _logger.LogDebug(
"Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
item.Id);
try try
{ {
@ -392,7 +403,13 @@ namespace Emby.Server.Implementations.Library
{ {
try try
{ {
_logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName); _logger.LogInformation(
"Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
if (fileSystemInfo.IsDirectory) if (fileSystemInfo.IsDirectory)
{ {
Directory.Delete(fileSystemInfo.FullName, true); Directory.Delete(fileSystemInfo.FullName, true);
@ -430,7 +447,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id); _itemRepository.DeleteItem(child.Id);
} }
_libraryItemsCache.TryRemove(item.Id, out BaseItem removed); _memoryCache.Remove(item.Id);
ReportItemRemoved(item, parent); ReportItemRemoved(item, parent);
} }
@ -500,8 +517,8 @@ namespace Emby.Server.Implementations.Library
{ {
// Try to normalize paths located underneath program-data in an attempt to make them more portable // 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(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
.TrimStart(new[] { '/', '\\' }) .TrimStart('/', '\\')
.Replace("/", "\\"); .Replace('/', '\\');
} }
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds) if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@ -514,8 +531,8 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5(); return key.GetMD5();
} }
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, bool allowIgnorePath = true) public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
=> ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent, allowIgnorePath: allowIgnorePath); => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
private BaseItem ResolvePath( private BaseItem ResolvePath(
FileSystemMetadata fileInfo, FileSystemMetadata fileInfo,
@ -523,8 +540,7 @@ namespace Emby.Server.Implementations.Library
IItemResolver[] resolvers, IItemResolver[] resolvers,
Folder parent = null, Folder parent = null,
string collectionType = null, string collectionType = null,
LibraryOptions libraryOptions = null, LibraryOptions libraryOptions = null)
bool allowIgnorePath = true)
{ {
if (fileInfo == null) if (fileInfo == null)
{ {
@ -548,7 +564,7 @@ namespace Emby.Server.Implementations.Library
}; };
// Return null if ignore rules deem that we should do so // Return null if ignore rules deem that we should do so
if (allowIgnorePath && IgnoreFile(args.FileInfo, args.Parent)) if (IgnoreFile(args.FileInfo, args.Parent))
{ {
return null; return null;
} }
@ -713,7 +729,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(rootFolderPath); Directory.CreateDirectory(rootFolderPath);
var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath), allowIgnorePath: false)) ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
.DeepCopy<Folder, AggregateFolder>(); .DeepCopy<Folder, AggregateFolder>();
// In case program data folder was moved // In case program data folder was moved
@ -765,14 +781,11 @@ namespace Emby.Server.Implementations.Library
return rootFolder; return rootFolder;
} }
private volatile UserRootFolder _userRootFolder;
private readonly object _syncLock = new object();
public Folder GetUserRootFolder() public Folder GetUserRootFolder()
{ {
if (_userRootFolder == null) if (_userRootFolder == null)
{ {
lock (_syncLock) lock (_userRootFolderSyncLock)
{ {
if (_userRootFolder == null) if (_userRootFolder == null)
{ {
@ -795,7 +808,7 @@ namespace Emby.Server.Implementations.Library
if (tmpItem == null) if (tmpItem == null)
{ {
_logger.LogDebug("Creating new userRootFolder with DeepCopy"); _logger.LogDebug("Creating new userRootFolder with DeepCopy");
tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath), allowIgnorePath: false)).DeepCopy<Folder, UserRootFolder>(); tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
} }
// In case program data folder was moved // In case program data folder was moved
@ -1235,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
if (_libraryItemsCache.TryGetValue(id, out BaseItem item)) if (_memoryCache.TryGetValue(id, out BaseItem item))
{ {
return item; return item;
} }
@ -1322,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
return new QueryResult<BaseItem> return new QueryResult<BaseItem>
{ {
Items = _itemRepository.GetItemList(query).ToArray() Items = _itemRepository.GetItemList(query)
}; };
} }
@ -1453,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItems(query); return _itemRepository.GetItems(query);
} }
var list = _itemRepository.GetItemList(query);
return new QueryResult<BaseItem> return new QueryResult<BaseItem>
{ {
Items = list Items = _itemRepository.GetItemList(query)
}; };
} }
@ -1580,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user) public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{ {
var tasks = IntroProviders var tasks = IntroProviders
.OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
.Take(1) .Take(1)
.Select(i => GetIntros(i, item, user)); .Select(i => GetIntros(i, item, user));
@ -1866,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
} }
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
if (outdated.Length == 0) // Skip image processing if current or live tv source
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{ {
RegisterItem(item); RegisterItem(item);
return; return;
@ -1894,9 +1905,19 @@ namespace Emby.Server.Implementations.Library
} }
} }
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image); try
image.Width = size.Width; {
image.Height = size.Height; ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
image.Width = size.Width;
image.Height = size.Height;
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path);
image.Width = 0;
image.Height = 0;
continue;
}
try try
{ {
@ -1925,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Updates the item. /// Updates the item.
/// </summary> /// </summary>
public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{ {
// Don't iterate multiple times foreach (var item in items)
var itemsList = items.ToList();
foreach (var item in itemsList)
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
{ {
@ -1942,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
} }
_itemRepository.SaveItems(itemsList, cancellationToken); _itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null) if (ItemUpdated != null)
{ {
foreach (var item in itemsList) foreach (var item in items)
{ {
// With the live tv guide this just creates too much noise // With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library) if (item.SourceType != SourceType.Library)
@ -2169,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => !string.IsNullOrEmpty(i)); .FirstOrDefault(i => !string.IsNullOrEmpty(i));
} }
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
public UserView GetNamedView( public UserView GetNamedView(
User user, User user,
string name, string name,
@ -2468,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
var episodeInfo = episode.IsFileProtocol ? var episodeInfo = episode.IsFileProtocol
resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) : ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
new Naming.TV.EpisodeInfo(); : new Naming.TV.EpisodeInfo();
if (episodeInfo == null)
{
episodeInfo = new Naming.TV.EpisodeInfo();
}
try try
{ {
@ -2483,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{ {
// Read from metadata // Read from metadata
var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest var mediaInfo = _mediaEncoder.GetMediaInfo(
{ new MediaInfoRequest
MediaSource = episode.GetMediaSources(false)[0], {
MediaType = DlnaProfileType.Video MediaSource = episode.GetMediaSources(false)[0],
}, CancellationToken.None).GetAwaiter().GetResult(); MediaType = DlnaProfileType.Video
},
CancellationToken.None).GetAwaiter().GetResult();
if (mediaInfo.ParentIndexNumber > 0) if (mediaInfo.ParentIndexNumber > 0)
{ {
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@ -2645,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
var videos = videoListResolver.Resolve(fileSystemChildren); var videos = videoListResolver.Resolve(fileSystemChildren);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase)); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null) if (currentVideo != null)
{ {
@ -2662,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
.Select(video => .Select(video =>
{ {
// Try to retrieve it from the db. If we don't find it, use the resolved version // Try to retrieve it from the db. If we don't find it, use the resolved version
var dbItem = GetItemById(video.Id) as Trailer; if (GetItemById(video.Id) is Trailer dbItem)
if (dbItem != null)
{ {
video = dbItem; video = dbItem;
} }
@ -2991,23 +3002,6 @@ namespace Emby.Server.Implementations.Library
}); });
} }
private static bool ValidateNetworkPath(string path)
{
// if (Environment.OSVersion.Platform == PlatformID.Win32NT)
//{
// // We can't validate protocol-based paths, so just allow them
// if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
// {
// return Directory.Exists(path);
// }
//}
// Without native support for unc, we cannot validate this when running under mono
return true;
}
private const string ShortcutFileExtension = ".mblink";
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{ {
AddMediaPathInternal(virtualFolderName, pathInfo, true); AddMediaPathInternal(virtualFolderName, pathInfo, true);
@ -3032,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The path does not exist."); throw new FileNotFoundException("The path does not exist.");
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3075,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(pathInfo)); throw new ArgumentNullException(nameof(pathInfo));
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3211,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
if (!Directory.Exists(virtualFolderPath)) if (!Directory.Exists(virtualFolderPath))
{ {
throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName)); throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
} }
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)

View File

@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
{ {
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IJsonSerializer _json;
private IJsonSerializer _json; private readonly IApplicationPaths _appPaths;
private IApplicationPaths _appPaths;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths) public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
{ {
@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000; mediaSource.AnalyzeDurationMs = 3000;
mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest mediaInfo = await _mediaEncoder.GetMediaInfo(
{ new MediaInfoRequest
MediaSource = mediaSource, {
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, MediaSource = mediaSource,
ExtractChapters = false MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false
}, cancellationToken).ConfigureAwait(false); },
cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null) if (cacheFilePath != null)
{ {
@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.RunTimeTicks = null; mediaSource.RunTimeTicks = null;
} }
var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1) if (audioStream == null || audioStream.Index == -1)
{ {
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.DefaultAudioStreamIndex = audioStream.Index; mediaSource.DefaultAudioStreamIndex = audioStream.Index;
} }
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null) if (videoStream != null)
{ {
if (!videoStream.BitRate.HasValue) if (!videoStream.BitRate.HasValue)

View File

@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
{ {
public class MediaSourceManager : IMediaSourceManager, IDisposable public class MediaSourceManager : IMediaSourceManager, IDisposable
{ {
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char LiveStreamIdDelimeter = '_';
private readonly IItemRepository _itemRepo; private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -40,6 +43,9 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private IMediaSourceProvider[] _providers; private IMediaSourceProvider[] _providers;
public MediaSourceManager( public MediaSourceManager(
@ -368,7 +374,6 @@ namespace Emby.Server.Implementations.Library
} }
} }
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
@ -451,9 +456,6 @@ namespace Emby.Server.Implementations.Library
.ToList(); .ToList();
} }
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken) public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{ {
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@ -619,12 +621,14 @@ namespace Emby.Server.Implementations.Library
if (liveStreamInfo is IDirectStreamProvider) if (liveStreamInfo is IDirectStreamProvider)
{ {
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest var info = await _mediaEncoder.GetMediaInfo(
{ new MediaInfoRequest
MediaSource = mediaSource, {
ExtractChapters = false, MediaSource = mediaSource,
MediaType = DlnaProfileType.Video ExtractChapters = false,
}, cancellationToken).ConfigureAwait(false); MediaType = DlnaProfileType.Video
},
cancellationToken).ConfigureAwait(false);
mediaSource.MediaStreams = info.MediaStreams; mediaSource.MediaStreams = info.MediaStreams;
mediaSource.Container = info.Container; mediaSource.Container = info.Container;
@ -855,24 +859,21 @@ namespace Emby.Server.Implementations.Library
} }
} }
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. private (IMediaSourceProvider, string) GetProvider(string key)
private const char LiveStreamIdDelimeter = '_';
private Tuple<IMediaSourceProvider, string> GetProvider(string key)
{ {
if (string.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key))
{ {
throw new ArgumentException("key"); throw new ArgumentException("Key can't be empty.", nameof(key));
} }
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2); var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
var splitIndex = key.IndexOf(LiveStreamIdDelimeter); var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1); var keyId = key.Substring(splitIndex + 1);
return new Tuple<IMediaSourceProvider, string>(provider, keyId); return (provider, keyId);
} }
/// <summary> /// <summary>
@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this);
} }
private readonly object _disposeLock = new object();
/// <summary> /// <summary>
/// Releases unmanaged and - optionally - managed resources. /// Releases unmanaged and - optionally - managed resources.
/// </summary> /// </summary>
@ -892,15 +893,12 @@ namespace Emby.Server.Implementations.Library
{ {
if (dispose) if (dispose)
{ {
lock (_disposeLock) foreach (var key in _openStreams.Keys.ToList())
{ {
foreach (var key in _openStreams.Keys.ToList()) CloseLiveStream(key).GetAwaiter().GetResult();
{
var task = CloseLiveStream(key);
Task.WaitAll(task);
}
} }
_liveStreamSemaphore.Dispose();
} }
} }
} }

View File

@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
} }
// load forced subs if we have found no suitable full subtitles // load forced subs if we have found no suitable full subtitles
stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)); stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
if (stream != null) if (stream != null)
{ {

View File

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;

View File

@ -1,6 +1,5 @@
using System.Globalization; using System.Globalization;
using Emby.Naming.TV; using Emby.Naming.TV;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary> /// </summary>
public class SeasonResolver : FolderResolver<Season> public class SeasonResolver : FolderResolver<Season>
{ {
private readonly IServerConfigurationManager _config;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly ILogger<SeasonResolver> _logger; private readonly ILogger<SeasonResolver> _logger;
@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SeasonResolver"/> class. /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
/// </summary> /// </summary>
/// <param name="config">The config.</param>
/// <param name="libraryManager">The library manager.</param> /// <param name="libraryManager">The library manager.</param>
/// <param name="localization">The localization.</param> /// <param name="localization">The localization.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
public SeasonResolver( public SeasonResolver(
IServerConfigurationManager config,
ILibraryManager libraryManager, ILibraryManager libraryManager,
ILocalizationManager localization, ILocalizationManager localization,
ILogger<SeasonResolver> logger) ILogger<SeasonResolver> logger)
{ {
_config = config;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_localization = localization; _localization = localization;
_logger = logger; _logger = logger;

View File

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search; using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
{ {
public class SearchEngine : ISearchEngine public class SearchEngine : ISearchEngine
{ {
private readonly ILogger<SearchEngine> _logger;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager) public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
{ {
_logger = logger;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
} }
@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{ {
User user = null; User user = null;
if (query.UserId != Guid.Empty)
if (query.UserId.Equals(Guid.Empty))
{
}
else
{ {
user = _userManager.GetUserById(query.UserId); user = _userManager.GetUserById(query.UserId);
} }
@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
if (query.StartIndex.HasValue) if (query.StartIndex.HasValue)
{ {
results = results.Skip(query.StartIndex.Value).ToList(); results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
} }
if (query.Limit.HasValue) if (query.Limit.HasValue)
{ {
results = results.Take(query.Limit.Value).ToList(); results = results.GetRange(0, query.Limit.Value);
} }
return new QueryResult<SearchHintInfo> return new QueryResult<SearchHintInfo>
{ {
TotalRecordCount = totalRecordCount, TotalRecordCount = totalRecordCount,
Items = results.ToArray() Items = results
}; };
} }
@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
if (string.IsNullOrEmpty(searchTerm)) if (string.IsNullOrEmpty(searchTerm))
{ {
throw new ArgumentNullException("SearchTerm can't be empty.", nameof(searchTerm)); throw new ArgumentException("SearchTerm can't be empty.", nameof(query));
} }
searchTerm = searchTerm.Trim().RemoveDiacritics(); searchTerm = searchTerm.Trim().RemoveDiacritics();

View File

@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book; using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, UserItemData> _userData = private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
private readonly ILogger<UserDataManager> _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserDataRepository _repository; private readonly IUserDataRepository _repository;
public UserDataManager( public UserDataManager(
ILogger<UserDataManager> logger,
IServerConfigurationManager config, IServerConfigurationManager config,
IUserManager userManager, IUserManager userManager,
IUserDataRepository repository) IUserDataRepository repository)
{ {
_logger = logger;
_config = config; _config = config;
_userManager = userManager; _userManager = userManager;
_repository = repository; _repository = repository;

View File

@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;

View File

@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
ListingsProviderInfo info, ListingsProviderInfo info,
List<string> programIds, List<string> programIds,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (programIds.Count == 0) if (programIds.Count == 0)
{ {
@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
var imageId = i.Substring(0, 10); var imageId = i.Substring(0, 10);
if (!imageIdString.Contains(imageId)) if (!imageIdString.Contains(imageId, StringComparison.Ordinal))
{ {
imageIdString += "\"" + imageId + "\","; imageIdString += "\"" + imageId + "\",";
} }

View File

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IChannelManager _channelManager; private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService; private readonly LiveTvDtoService _tvDtoService;
@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
ILibraryManager libraryManager, ILibraryManager libraryManager,
ITaskManager taskManager, ITaskManager taskManager,
ILocalizationManager localization, ILocalizationManager localization,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem, IFileSystem fileSystem,
IChannelManager channelManager, IChannelManager channelManager,
LiveTvDtoService liveTvDtoService) LiveTvDtoService liveTvDtoService)
@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
_libraryManager = libraryManager; _libraryManager = libraryManager;
_taskManager = taskManager; _taskManager = taskManager;
_localization = localization; _localization = localization;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_dtoService = dtoService; _dtoService = dtoService;
_userDataManager = userDataManager; _userDataManager = userDataManager;
@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
{ {
info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info)); info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info)); info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));

View File

@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv
public class LiveTvMediaSourceProvider : IMediaSourceProvider public class LiveTvMediaSourceProvider : IMediaSourceProvider
{ {
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char StreamIdDelimeter = '_'; private const char StreamIdDelimiter = '_';
private const string StreamIdDelimeterString = "_";
private readonly ILiveTvManager _liveTvManager; private readonly ILiveTvManager _liveTvManager;
private readonly ILogger<LiveTvMediaSourceProvider> _logger; private readonly ILogger<LiveTvMediaSourceProvider> _logger;
@ -47,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv
} }
} }
return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>()); return Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
} }
private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv
source.Id ?? string.Empty source.Id ?? string.Empty
}; };
source.OpenToken = string.Join(StreamIdDelimeterString, openKeys); source.OpenToken = string.Join(StreamIdDelimiter, openKeys);
} }
// Dummy this up so that direct play checks can still run // Dummy this up so that direct play checks can still run
@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
/// <inheritdoc /> /// <inheritdoc />
public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{ {
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3); var keys = openToken.Split(StreamIdDelimiter, 3);
var mediaSourceId = keys.Length >= 3 ? keys[2] : null; var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);

View File

@ -1,10 +1,10 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -14,7 +14,7 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts
@ -23,17 +23,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
protected readonly IServerConfigurationManager Config; protected readonly IServerConfigurationManager Config;
protected readonly ILogger<BaseTunerHost> Logger; protected readonly ILogger<BaseTunerHost> Logger;
protected IJsonSerializer JsonSerializer;
protected readonly IFileSystem FileSystem; protected readonly IFileSystem FileSystem;
private readonly ConcurrentDictionary<string, ChannelCache> _channelCache = private readonly IMemoryCache _memoryCache;
new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem) protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
{ {
Config = config; Config = config;
Logger = logger; Logger = logger;
JsonSerializer = jsonSerializer; _memoryCache = memoryCache;
FileSystem = fileSystem; FileSystem = fileSystem;
} }
@ -44,23 +42,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken) public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{ {
ChannelCache cache = null;
var key = tuner.Id; var key = tuner.Id;
if (enableCache && !string.IsNullOrEmpty(key) && _channelCache.TryGetValue(key, out cache)) if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
{ {
return cache.Channels.ToList(); return cache;
} }
var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false); var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
var list = result.ToList();
// logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); // logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
if (!string.IsNullOrEmpty(key) && list.Count > 0) if (!string.IsNullOrEmpty(key) && list.Count > 0)
{ {
cache = cache ?? new ChannelCache(); _memoryCache.Set(key, list);
cache.Channels = list;
_channelCache.AddOrUpdate(key, cache, (k, v) => cache);
} }
return list; return list;
@ -95,7 +89,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try try
{ {
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile)); Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
JsonSerializer.SerializeToFile(channels, channelCacheFile); await using var writeStream = File.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
} }
catch (IOException) catch (IOException)
{ {
@ -110,7 +105,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
try try
{ {
var channels = JsonSerializer.DeserializeFromFile<List<ChannelInfo>>(channelCacheFile); await using var readStream = File.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels); list.AddRange(channels);
} }
catch (IOException) catch (IOException)
@ -233,10 +230,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
return Config.GetConfiguration<LiveTvOptions>("livetv"); return Config.GetConfiguration<LiveTvOptions>("livetv");
} }
private class ChannelCache
{
public List<ChannelInfo> Channels;
}
} }
} }

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -23,7 +24,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@ -39,14 +40,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public HdHomerunHost( public HdHomerunHost(
IServerConfigurationManager config, IServerConfigurationManager config,
ILogger<HdHomerunHost> logger, ILogger<HdHomerunHost> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem, IFileSystem fileSystem,
IHttpClient httpClient, IHttpClient httpClient,
IServerApplicationHost appHost, IServerApplicationHost appHost,
ISocketFactory socketFactory, ISocketFactory socketFactory,
INetworkManager networkManager, INetworkManager networkManager,
IStreamHelper streamHelper) IStreamHelper streamHelper,
: base(config, logger, jsonSerializer, fileSystem) IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_appHost = appHost; _appHost = appHost;
@ -75,18 +76,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
BufferContent = false BufferContent = false
}; };
using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false)) using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
using (var stream = response.Content) await using var stream = response.Content;
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
if (info.ImportFavoritesOnly)
{ {
var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); lineup = lineup.Where(i => i.Favorite).ToList();
if (info.ImportFavoritesOnly)
{
lineup = lineup.Where(i => i.Favorite).ToList();
}
return lineup.Where(i => !i.DRM).ToList();
} }
return lineup.Where(i => !i.DRM).ToList();
} }
private class HdHomerunChannelInfo : ChannelInfo private class HdHomerunChannelInfo : ChannelInfo
@ -132,30 +132,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try try
{ {
using (var response = await _httpClient.SendAsync(new HttpRequestOptions() using var response = await _httpClient.SendAsync(
new HttpRequestOptions
{ {
Url = string.Format("{0}/discover.json", GetApiUrl(info)), Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)),
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
BufferContent = false BufferContent = false
}, HttpMethod.Get).ConfigureAwait(false)) }, HttpMethod.Get).ConfigureAwait(false);
using (var stream = response.Content) await using var stream = response.Content;
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
{ {
var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); lock (_modelCache)
if (!string.IsNullOrEmpty(cacheKey))
{ {
lock (_modelCache) _modelCache[cacheKey] = discoverResponse;
{
_modelCache[cacheKey] = discoverResponse;
}
} }
return discoverResponse;
} }
return discoverResponse;
} }
catch (HttpException ex) catch (HttpException ex)
{ {
if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
{ {
var defaultValue = "HDHR"; var defaultValue = "HDHR";
var response = new DiscoverResponse var response = new DiscoverResponse
@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
while (!sr.EndOfStream) while (!sr.EndOfStream)
{ {
string line = StripXML(sr.ReadLine()); string line = StripXML(sr.ReadLine());
if (line.Contains("Channel")) if (line.Contains("Channel", StringComparison.Ordinal))
{ {
LiveTvTunerStatus status; LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static string StripXML(string source) private static string StripXML(string source)
{ {
if (string.IsNullOrEmpty(source))
{
return string.Empty;
}
char[] buffer = new char[source.Length]; char[] buffer = new char[source.Length];
int bufferIndex = 0; int bufferIndex = 0;
bool inside = false; bool inside = false;
@ -270,7 +275,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
for (int i = 0; i < model.TunerCount; ++i) for (int i = 0; i < model.TunerCount; ++i)
{ {
var name = string.Format("Tuner {0}", i + 1); var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
var currentChannel = "none"; // @todo Get current channel and map back to Station Id var currentChannel = "none"; // @todo Get current channel and map back to Station Id
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;

View File

@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
public class HdHomerunManager : IDisposable public sealed class HdHomerunManager : IDisposable
{ {
public const int HdHomeRunPort = 65001; public const int HdHomeRunPort = 65001;
@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult(); StopStreaming(socket).GetAwaiter().GetResult();
} }
} }
GC.SuppressFinalize(this);
} }
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
_activeTuner = i; _activeTuner = i;
var lockKeyString = string.Format("{0:d}", lockKeyValue); var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
continue; continue;
} }
var commandList = commands.GetCommands(); foreach (var command in commands.GetCommands())
foreach (var command in commandList)
{ {
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort); var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);

View File

@ -18,7 +18,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -36,13 +36,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IServerConfigurationManager config, IServerConfigurationManager config,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
ILogger<M3UTunerHost> logger, ILogger<M3UTunerHost> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem, IFileSystem fileSystem,
IHttpClient httpClient, IHttpClient httpClient,
IServerApplicationHost appHost, IServerApplicationHost appHost,
INetworkManager networkManager, INetworkManager networkManager,
IStreamHelper streamHelper) IStreamHelper streamHelper,
: base(config, logger, jsonSerializer, fileSystem) IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_appHost = appHost; _appHost = appHost;

View File

@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl) private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
{ {
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null; var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
string numberString = null; string numberString = null;
string attributeValue; string attributeValue;
double doubleValue;
if (attributes.TryGetValue("tvg-chno", out attributeValue)) if (attributes.TryGetValue("tvg-chno", out attributeValue))
{ {
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{ {
numberString = attributeValue; numberString = attributeValue;
} }
@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
if (attributes.TryGetValue("tvg-id", out attributeValue)) if (attributes.TryGetValue("tvg-id", out attributeValue))
{ {
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{ {
numberString = attributeValue; numberString = attributeValue;
} }
else if (attributes.TryGetValue("channel-id", out attributeValue)) else if (attributes.TryGetValue("channel-id", out attributeValue))
{ {
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{ {
numberString = attributeValue; numberString = attributeValue;
} }
} }
} }
if (String.IsNullOrWhiteSpace(numberString)) if (string.IsNullOrWhiteSpace(numberString))
{ {
// Using this as a fallback now as this leads to Problems with channels like "5 USA" // Using this as a fallback now as this leads to Problems with channels like "5 USA"
// where 5 isnt ment to be the channel number // where 5 isnt ment to be the channel number
// Check for channel number with the format from SatIp // Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz // #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz // #EXTINF:0,84.0 - VOX Schweiz
if (!string.IsNullOrWhiteSpace(nameInExtInf)) if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace())
{ {
var numberIndex = nameInExtInf.IndexOf(' '); var numberIndex = nameInExtInf.IndexOf(' ');
if (numberIndex > 0) if (numberIndex > 0)
{ {
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{ {
numberString = numberPart; numberString = numberPart.ToString();
} }
} }
} }
@ -231,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
try try
{ {
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last()); numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
if (!IsValidChannelNumber(numberString)) if (!IsValidChannelNumber(numberString))
{ {
@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return false; return false;
} }
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{ {
return false; return false;
} }
@ -281,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{ {
// channel.Number = number.ToString(); // channel.Number = number.ToString();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });

View File

@ -19,8 +19,8 @@
"Sync": "Sinkroniseer", "Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies", "HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies", "Songs": "Liedjies",
"DeviceOnlineWithName": "{0} is verbind", "DeviceOnlineWithName": "{0} gekoppel is",
"DeviceOfflineWithName": "{0} het afgesluit", "DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings", "Collections": "Versamelings",
"Inherit": "Ontvang", "Inherit": "Ontvang",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "Live TV",
@ -91,5 +91,9 @@
"ChapterNameValue": "Hoofstuk", "ChapterNameValue": "Hoofstuk",
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}", "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer", "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
"Albums": "Albums" "Albums": "Albums",
"TasksChannelsCategory": "Internet kanale",
"TasksApplicationCategory": "aansoek",
"TasksLibraryCategory": "biblioteek",
"TasksMaintenanceCategory": "onderhoud"
} }

View File

@ -1,5 +1,5 @@
{ {
"Albums": "ألبومات", "Albums": "البومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}", "AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق", "Application": "تطبيق",
"Artists": "الفنانين", "Artists": "الفنانين",
@ -14,7 +14,7 @@
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
"Favorites": "المفضلة", "Favorites": "المفضلة",
"Folders": "المجلدات", "Folders": "المجلدات",
"Genres": "الأنواع", "Genres": "التضنيفات",
"HeaderAlbumArtists": "فناني الألبومات", "HeaderAlbumArtists": "فناني الألبومات",
"HeaderCameraUploads": "تحميلات الكاميرا", "HeaderCameraUploads": "تحميلات الكاميرا",
"HeaderContinueWatching": "استئناف", "HeaderContinueWatching": "استئناف",
@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي", "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
"NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي", "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي",
"NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا", "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا",
"NotificationOptionInstallationFailed": "فشل في التثبيت", "NotificationOptionInstallationFailed": "فشل التثبيت",
"NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد", "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد",
"NotificationOptionPluginError": "فشل في البرنامج المضاف", "NotificationOptionPluginError": "فشل في البرنامج المضاف",
"NotificationOptionPluginInstalled": "تم تثبيت الملحق", "NotificationOptionPluginInstalled": "تم تثبيت الملحق",

View File

@ -1,26 +1,26 @@
{ {
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে", "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে", "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
"Collections": "সংকলন", "Collections": "কলেক্শন",
"ChapterNameValue": "অধ্যায় {0}", "ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল", "Channels": "চ্যানেল",
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "বই", "Books": "বই",
"AuthenticationSucceededWithUserName": "{0} যাচাই সফল", "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
"Artists": "শিল্পী", "Artists": "শিল্পীরা",
"Application": "অ্যাপ্লিকেশন", "Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামগুলো", "Albums": "অ্যালবামগুলো",
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন", "HeaderContinueWatching": "দেখতে থাকুন",
"HeaderCameraUploads": "ক্যামেরার আপলোডগুলো", "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
"HeaderAlbumArtists": "এলবামের শিল্পী", "HeaderAlbumArtists": "এলবাম শিল্পী",
"Genres": "ঘরানা", "Genres": "জেনার",
"Folders": "ফোল্ডারগুলো", "Folders": "ফোল্ডারগুলো",
"Favorites": "ফেভারিটগুলো", "Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "প: {0}, ডিভাইস: {0}", "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"VersionNumber": "সংস্করণ {0}", "VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ - {0}", "ValueSpecialEpisodeName": "বিশেষ - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
@ -74,20 +74,20 @@
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ", "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
"MusicVideos": "গানের ভিডিও", "MusicVideos": "গানের ভিডিও",
"Music": "গান", "Music": "গান",
"Movies": "সিনেমা", "Movies": "চলচ্চিত্র",
"MixedContent": "মিশ্র কন্টেন্ট", "MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপ", "HeaderRecordingGroups": "রেকর্ডিং দল",
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে", "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে", "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
"MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে", "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
"Latest": "একদম নতুন", "Latest": "সর্বশেষ",
"LabelRunningTimeValue": "চলার সময়: {0}", "LabelRunningTimeValue": "চলার সময়: {0}",
"LabelIpAddressValue": "আইপি ঠিকানা: {0}", "LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে", "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে", "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "থেকে পাওয়া", "Inherit": "থেকে পাওয়া",
"HomeVideos": "বাসার ভিডিও", "HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে", "HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি", "HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো", "HeaderFavoriteSongs": "প্রিয় গানগুলো",

View File

@ -3,9 +3,9 @@
"AppDeviceValues": "App: {0}, Gerät: {1}", "AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung", "Application": "Anwendung",
"Artists": "Interpreten", "Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert", "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
"Books": "Bücher", "Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
"Channels": "Kanäle", "Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}", "ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen", "Collections": "Sammlungen",
@ -101,12 +101,12 @@
"TaskCleanTranscode": "Lösche Transkodier Pfad", "TaskCleanTranscode": "Lösche Transkodier Pfad",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
"TaskUpdatePlugins": "Update Plugins", "TaskUpdatePlugins": "Update Plugins",
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.", "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
"TaskRefreshPeople": "Erneuere Schausteller", "TaskRefreshPeople": "Erneuere Schauspieler",
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.", "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
"TaskCleanLogs": "Lösche Log Pfad", "TaskCleanLogs": "Lösche Log Pfad",
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.", "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
"TaskRefreshLibrary": "Scanne alle Bibliotheken", "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
"TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.", "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder", "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
"TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.", "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",

View File

@ -31,7 +31,7 @@
"ItemAddedWithName": "{0} fue agregado a la biblioteca", "ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca", "ItemRemovedWithName": "{0} fue removido de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}", "LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Duración: {0}", "LabelRunningTimeValue": "Tiempo de reproducción: {0}",
"Latest": "Recientes", "Latest": "Recientes",
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado", "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}", "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",

View File

@ -18,13 +18,13 @@
"HeaderAlbumArtists": "אמני האלבום", "HeaderAlbumArtists": "אמני האלבום",
"HeaderCameraUploads": "העלאות ממצלמה", "HeaderCameraUploads": "העלאות ממצלמה",
"HeaderContinueWatching": "המשך לצפות", "HeaderContinueWatching": "המשך לצפות",
"HeaderFavoriteAlbums": "אלבומים שאהבתי", "HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים", "HeaderFavoriteArtists": "אמנים מועדפים",
"HeaderFavoriteEpisodes": "פרקים מועדפים", "HeaderFavoriteEpisodes": "פרקים מועדפים",
"HeaderFavoriteShows": "סדרות מועדפות", "HeaderFavoriteShows": "תוכניות מועדפות",
"HeaderFavoriteSongs": "שירים מועדפים", "HeaderFavoriteSongs": "שירים מועדפים",
"HeaderLiveTV": "שידורים חיים", "HeaderLiveTV": "שידורים חיים",
"HeaderNextUp": "הבא", "HeaderNextUp": "הבא בתור",
"HeaderRecordingGroups": "קבוצות הקלטה", "HeaderRecordingGroups": "קבוצות הקלטה",
"HomeVideos": "סרטונים בייתים", "HomeVideos": "סרטונים בייתים",
"Inherit": "הורש", "Inherit": "הורש",
@ -45,37 +45,37 @@
"NameSeasonNumber": "עונה {0}", "NameSeasonNumber": "עונה {0}",
"NameSeasonUnknown": "עונה לא ידועה", "NameSeasonUnknown": "עונה לא ידועה",
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.", "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
"NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
"NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
"NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlayback": "ניגון שמע החל",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
"NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
"NotificationOptionInstallationFailed": "התקנה נכשלה", "NotificationOptionInstallationFailed": "התקנה נכשלה",
"NotificationOptionNewLibraryContent": "New content added", "NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
"NotificationOptionPluginError": "Plugin failure", "NotificationOptionPluginError": "כשלון בתוסף",
"NotificationOptionPluginInstalled": "התוסף הותקן", "NotificationOptionPluginInstalled": "התוסף הותקן",
"NotificationOptionPluginUninstalled": "התוסף הוסר", "NotificationOptionPluginUninstalled": "התוסף הוסר",
"NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן", "NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
"NotificationOptionServerRestartRequired": "יש לאתחל את השרת", "NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
"NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
"NotificationOptionUserLockedOut": "User locked out", "NotificationOptionUserLockedOut": "משתמש ננעל",
"NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlayback": "ניגון וידאו החל",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped", "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
"Photos": "תמונות", "Photos": "תמונות",
"Playlists": "רשימות הפעלה", "Playlists": "רשימות הפעלה",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed", "PluginInstalledWithName": "{0} הותקן",
"PluginUninstalledWithName": "{0} was uninstalled", "PluginUninstalledWithName": "{0} הוסר",
"PluginUpdatedWithName": "{0} was updated", "PluginUpdatedWithName": "{0} עודכן",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskFailedWithName": "{0} נכשל",
"ScheduledTaskStartedWithName": "{0} started", "ScheduledTaskStartedWithName": "{0} החל",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות", "Shows": "סדרות",
"Songs": "שירים", "Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.", "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
"Sync": "סנכרן", "Sync": "סנכרן",
"System": "System", "System": "System",
"TvShows": "סדרות טלוויזיה", "TvShows": "סדרות טלוויזיה",
@ -83,14 +83,14 @@
"UserCreatedWithName": "המשתמש {0} נוצר", "UserCreatedWithName": "המשתמש {0} נוצר",
"UserDeletedWithName": "המשתמש {0} הוסר", "UserDeletedWithName": "המשתמש {0} הוסר",
"UserDownloadingItemWithValues": "{0} מוריד את {1}", "UserDownloadingItemWithValues": "{0} מוריד את {1}",
"UserLockedOutWithName": "User {0} has been locked out", "UserLockedOutWithName": "המשתמש {0} ננעל",
"UserOfflineFromDevice": "{0} has disconnected from {1}", "UserOfflineFromDevice": "{0} התנתק מ-{1}",
"UserOnlineFromDevice": "{0} is online from {1}", "UserOnlineFromDevice": "{0} מחובר מ-{1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}", "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}", "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}", "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}", "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
"ValueSpecialEpisodeName": "מיוחד- {0}", "ValueSpecialEpisodeName": "מיוחד- {0}",
"VersionNumber": "Version {0}", "VersionNumber": "Version {0}",
"TaskRefreshLibrary": "סרוק ספריית מדיה", "TaskRefreshLibrary": "סרוק ספריית מדיה",
@ -109,7 +109,7 @@
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט", "TasksChannelsCategory": "ערוצי אינטרנט",
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.", "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.", "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.", "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
"TaskRefreshChannels": "רענן ערוץ", "TaskRefreshChannels": "רענן ערוץ",
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.", "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",

View File

@ -7,8 +7,8 @@
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru", "Latest": "Terbaru",
"LabelIpAddressValue": "Alamat IP: {0}", "LabelIpAddressValue": "Alamat IP: {0}",
"ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan", "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
"ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan", "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
"Inherit": "Warisan", "Inherit": "Warisan",
"HomeVideos": "Video Rumah", "HomeVideos": "Video Rumah",
"HeaderRecordingGroups": "Grup Rekaman", "HeaderRecordingGroups": "Grup Rekaman",
@ -19,8 +19,8 @@
"HeaderFavoriteEpisodes": "Episode Favorit", "HeaderFavoriteEpisodes": "Episode Favorit",
"HeaderFavoriteArtists": "Artis Favorit", "HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit", "HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Masih Melihat", "HeaderContinueWatching": "Lanjutkan Menonton",
"HeaderCameraUploads": "Uplod Kamera", "HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis", "HeaderAlbumArtists": "Album Artis",
"Genres": "Genre", "Genres": "Genre",
"Folders": "Folder", "Folders": "Folder",
@ -32,11 +32,11 @@
"ChapterNameValue": "Bagian {0}", "ChapterNameValue": "Bagian {0}",
"Channels": "Saluran", "Channels": "Saluran",
"TvShows": "Seri TV", "TvShows": "Seri TV",
"SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}", "SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
"StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.", "StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
"Songs": "Lagu", "Songs": "Lagu",
"Playlists": "Daftar putar", "Playlists": "Daftar putar",
"NotificationOptionPluginUninstalled": "Plugin dilepas", "NotificationOptionPluginUninstalled": "Plugin dihapus",
"MusicVideos": "Video musik", "MusicVideos": "Video musik",
"VersionNumber": "Versi {0}", "VersionNumber": "Versi {0}",
"ValueSpecialEpisodeName": "Spesial - {0}", "ValueSpecialEpisodeName": "Spesial - {0}",
@ -65,7 +65,7 @@
"Photos": "Foto", "Photos": "Foto",
"NotificationOptionUserLockedOut": "Pengguna terkunci", "NotificationOptionUserLockedOut": "Pengguna terkunci",
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal", "NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
"NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan", "NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
"NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang", "NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
"NotificationOptionPluginInstalled": "Plugin terpasang", "NotificationOptionPluginInstalled": "Plugin terpasang",
"NotificationOptionPluginError": "Kegagalan plugin", "NotificationOptionPluginError": "Kegagalan plugin",
@ -74,14 +74,14 @@
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah", "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang", "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia", "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
"NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.", "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
"NameSeasonUnknown": "Musim tak diketahui", "NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}", "NameSeasonNumber": "Musim {0}",
"NameInstallFailed": "{0} instalasi gagal", "NameInstallFailed": "{0} penginstalan gagal",
"Music": "Musik", "Music": "Musik",
"Movies": "Film", "Movies": "Film",
"MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui", "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui", "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}", "FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}", "CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus", "DeviceOfflineWithName": "{0} telah terputus",
@ -90,6 +90,28 @@
"NotificationOptionVideoPlayback": "Pemutaran video dimulai", "NotificationOptionVideoPlayback": "Pemutaran video dimulai",
"NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti", "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
"NotificationOptionAudioPlayback": "Pemutaran audio dimulai", "NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
"MixedContent": "Konten campur", "MixedContent": "Konten campuran",
"PluginUninstalledWithName": "{0} telah dihapus" "PluginUninstalledWithName": "{0} telah dihapus",
"TaskRefreshChapterImagesDescription": "Membuat gambar mini untuk video yang memiliki bagian.",
"TaskRefreshChapterImages": "Ekstrak Gambar Bagian",
"TaskCleanCacheDescription": "Menghapus file cache yang tidak lagi dibutuhkan oleh sistem.",
"TaskCleanCache": "Bersihkan Cache Direktori",
"TasksLibraryCategory": "Pustaka",
"TasksMaintenanceCategory": "Perbaikan",
"TasksApplicationCategory": "Aplikasi",
"TaskRefreshPeopleDescription": "Memperbarui metadata untuk aktor dan sutradara di pustaka media Anda.",
"TaskRefreshLibraryDescription": "Memindai Pustaka media Anda untuk mencari file baru dan memperbarui metadata.",
"TasksChannelsCategory": "Saluran Online",
"TaskDownloadMissingSubtitlesDescription": "Mencari di internet untuk subtitle yang hilang berdasarkan konfigurasi metadata.",
"TaskDownloadMissingSubtitles": "Unduh subtitle yang hilang",
"TaskRefreshChannelsDescription": "Segarkan informasi saluran internet.",
"TaskRefreshChannels": "Segarkan Saluran",
"TaskCleanTranscodeDescription": "Menghapus file transcode yang berumur lebih dari satu hari.",
"TaskCleanTranscode": "Bersihkan Direktori Transcode",
"TaskUpdatePluginsDescription": "Unduh dan instal pembaruan untuk plugin yang dikonfigurasi untuk memperbarui secara otomatis.",
"TaskUpdatePlugins": "Perbarui Plugin",
"TaskRefreshPeople": "Muat ulang Orang",
"TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.",
"TaskCleanLogs": "Bersihkan Log Direktori",
"TaskRefreshLibrary": "Pindai Pustaka Media"
} }

View File

@ -84,8 +84,8 @@
"UserDeletedWithName": "L'utente {0} è stato rimosso", "UserDeletedWithName": "L'utente {0} è stato rimosso",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}", "UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato", "UserLockedOutWithName": "L'utente {0} è stato bloccato",
"UserOfflineFromDevice": "{0} è stato disconnesso da {1}", "UserOfflineFromDevice": "{0} si è disconnesso su {1}",
"UserOnlineFromDevice": "{0} è online da {1}", "UserOnlineFromDevice": "{0} è online su {1}",
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}", "UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}", "UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
"UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}", "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
@ -102,11 +102,11 @@
"TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.", "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
"TaskUpdatePlugins": "Aggiorna i Plugin", "TaskUpdatePlugins": "Aggiorna i Plugin",
"TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.", "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
"TaskRefreshPeople": "Aggiorna persone", "TaskRefreshPeople": "Aggiornamento Persone",
"TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.", "TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
"TaskCleanLogs": "Pulisci la cartella dei log", "TaskCleanLogs": "Pulisci la cartella dei log",
"TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.", "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
"TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali", "TaskRefreshLibrary": "Scan Librerie",
"TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.", "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
"TaskRefreshChapterImages": "Estrai immagini capitolo", "TaskRefreshChapterImages": "Estrai immagini capitolo",
"TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.", "TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",

View File

@ -57,5 +57,7 @@
"HeaderCameraUploads": "कॅमेरा अपलोड", "HeaderCameraUploads": "कॅमेरा अपलोड",
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे", "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
"Application": "अ‍ॅप्लिकेशन", "Application": "अ‍ॅप्लिकेशन",
"AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}" "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
"Collections": "संग्रह",
"ChapterNameValue": "धडा {0}"
} }

View File

@ -5,47 +5,47 @@
"Artists": "Artis", "Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan", "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku", "Books": "Buku-buku",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
"Channels": "Saluran", "Channels": "Saluran",
"ChapterNameValue": "Chapter {0}", "ChapterNameValue": "Bab {0}",
"Collections": "Koleksi", "Collections": "Koleksi",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} telah disambung",
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}", "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
"Favorites": "Favorites", "Favorites": "Kegemaran",
"Folders": "Folders", "Folders": "Fail-fail",
"Genres": "Genre-genre", "Genres": "Genre-genre",
"HeaderAlbumArtists": "Album Artists", "HeaderAlbumArtists": "Album Artis-artis",
"HeaderCameraUploads": "Muatnaik Kamera", "HeaderCameraUploads": "Muatnaik Kamera",
"HeaderContinueWatching": "Terus Menonton", "HeaderContinueWatching": "Terus Menonton",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Album-album Kegemaran",
"HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteArtists": "Artis-artis Kegemaran",
"HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
"HeaderFavoriteShows": "Favorite Shows", "HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
"HeaderFavoriteSongs": "Favorite Songs", "HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "TV Siaran Langsung",
"HeaderNextUp": "Next Up", "HeaderNextUp": "Seterusnya",
"HeaderRecordingGroups": "Recording Groups", "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
"HomeVideos": "Home videos", "HomeVideos": "Video Personal",
"Inherit": "Inherit", "Inherit": "Mewarisi",
"ItemAddedWithName": "{0} was added to the library", "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
"ItemRemovedWithName": "{0} was removed from the library", "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
"LabelIpAddressValue": "Alamat IP: {0}", "LabelIpAddressValue": "Alamat IP: {0}",
"LabelRunningTimeValue": "Running time: {0}", "LabelRunningTimeValue": "Masa berjalan: {0}",
"Latest": "Latest", "Latest": "Terbaru",
"MessageApplicationUpdated": "Jellyfin Server has been updated", "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
"MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
"MixedContent": "Mixed content", "MixedContent": "Kandungan campuran",
"Movies": "Movies", "Movies": "Filem",
"Music": "Muzik", "Music": "Muzik",
"MusicVideos": "Video muzik", "MusicVideos": "Video muzik",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Season {0}", "NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Season Unknown", "NameSeasonUnknown": "Musim Tidak Diketahui",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
"NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
"NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionApplicationUpdateInstalled": "Application update installed",
"NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlayback": "Audio playback started",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",

View File

@ -104,5 +104,14 @@
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.", "TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.", "TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
"TasksChannelsCategory": "Canais de Internet", "TasksChannelsCategory": "Canais de Internet",
"TaskRefreshChapterImages": "Extrair Imagens do Capítulo" "TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
"TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
"TaskDownloadMissingSubtitles": "Download das legendas em falta",
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
"TaskCleanTranscode": "Limpar o diretório de Transcode",
"TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
"TaskRefreshPeople": "Atualizar pessoas",
"TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados."
} }

View File

@ -45,7 +45,7 @@
"TvShows": "தொலைக்காட்சித் தொடர்கள்", "TvShows": "தொலைக்காட்சித் தொடர்கள்",
"Sync": "ஒத்திசைவு", "Sync": "ஒத்திசைவு",
"StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.", "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
"Songs": "பாடடுகள்", "Songs": "பாட்கள்",
"Shows": "தொடர்கள்", "Shows": "தொடர்கள்",
"ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்", "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
"ScheduledTaskStartedWithName": "{0} துவங்கியது", "ScheduledTaskStartedWithName": "{0} துவங்கியது",
@ -93,7 +93,25 @@
"Channels": "சேனல்கள்", "Channels": "சேனல்கள்",
"Books": "புத்தகங்கள்", "Books": "புத்தகங்கள்",
"AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது", "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
"Artists": "கலைஞர்கள்", "Artists": "கலைஞர்",
"Application": "செயலி", "Application": "செயலி",
"Albums": "ஆல்பங்கள்" "Albums": "ஆல்பங்கள்",
"NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
"MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
"TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
"TaskCleanLogs": "பதிவு அடைவு சுத்தம் செய்யுங்கள்",
"TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
"TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
"ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
"UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
"HomeVideos": "முகப்பு வீடியோக்கள்",
"UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
"UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
} }

View File

@ -67,5 +67,7 @@
"Artists": "นักแสดง", "Artists": "นักแสดง",
"Application": "แอปพลิเคชั่น", "Application": "แอปพลิเคชั่น",
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}", "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
"Albums": "อัลบั้ม" "Albums": "อัลบั้ม",
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
"ScheduledTaskFailedWithName": "{0} ล้มเหลว"
} }

View File

@ -1,13 +1,13 @@
{ {
"MusicVideos": "Музичні відео", "MusicVideos": "Музичні кліпи",
"Music": "Музика", "Music": "Музика",
"Movies": "Фільми", "Movies": "Фільми",
"MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}", "MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
"MessageApplicationUpdated": "Jellyfin Server був оновлений", "MessageApplicationUpdated": "Jellyfin Server оновлено",
"Latest": "Останні", "Latest": "Останні",
"LabelIpAddressValue": "IP-адреси: {0}", "LabelIpAddressValue": "IP-адреса: {0}",
"ItemRemovedWithName": "{0} видалено з бібліотеки", "ItemRemovedWithName": "{0} видалено з медіатеки",
"ItemAddedWithName": "{0} додано до бібліотеки", "ItemAddedWithName": "{0} додано до медіатеки",
"HeaderNextUp": "Наступний", "HeaderNextUp": "Наступний",
"HeaderLiveTV": "Ефірне ТБ", "HeaderLiveTV": "Ефірне ТБ",
"HeaderFavoriteSongs": "Улюблені пісні", "HeaderFavoriteSongs": "Улюблені пісні",
@ -17,20 +17,101 @@
"HeaderFavoriteAlbums": "Улюблені альбоми", "HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд", "HeaderContinueWatching": "Продовжити перегляд",
"HeaderCameraUploads": "Завантажено з камери", "HeaderCameraUploads": "Завантажено з камери",
"HeaderAlbumArtists": "Виконавці альбомів", "HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри", "Genres": "Жанри",
"Folders": "Директорії", "Folders": "Каталоги",
"Favorites": "Улюблені", "Favorites": "Улюблені",
"DeviceOnlineWithName": "{0} під'єднано", "DeviceOnlineWithName": "Пристрій {0} підключився",
"DeviceOfflineWithName": "{0} від'єднано", "DeviceOfflineWithName": "Пристрій {0} відключився",
"Collections": "Колекції", "Collections": "Колекції",
"ChapterNameValue": "Глава {0}", "ChapterNameValue": "Розділ {0}",
"Channels": "Канали", "Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}", "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
"Books": "Книги", "Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успішно авторизовані", "AuthenticationSucceededWithUserName": "{0} успішно авторизований",
"Artists": "Виконавці", "Artists": "Виконавці",
"Application": "Додаток", "Application": "Додаток",
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}", "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
"Albums": "Альбоми" "Albums": "Альбоми",
"NotificationOptionServerRestartRequired": "Необхідно перезапустити сервер",
"NotificationOptionPluginUpdateInstalled": "Встановлено оновлення плагіна",
"NotificationOptionPluginUninstalled": "Плагін видалено",
"NotificationOptionPluginInstalled": "Плагін встановлено",
"NotificationOptionPluginError": "Помилка плагіна",
"NotificationOptionNewLibraryContent": "Додано новий контент",
"HomeVideos": "Домашнє відео",
"FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
"LabelRunningTimeValue": "Тривалість: {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 Server завантажується. Будь ласка, спробуйте трішки пізніше.",
"Songs": "Пісні",
"Shows": "Шоу",
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
"ScheduledTaskStartedWithName": "{0} розпочато",
"ScheduledTaskFailedWithName": "Помилка {0}",
"ProviderValue": "Постачальник: {0}",
"PluginUpdatedWithName": "{0} оновлено",
"PluginUninstalledWithName": "{0} видалено",
"PluginInstalledWithName": "{0} встановлено",
"Plugin": "Плагін",
"Playlists": "Плейлисти",
"Photos": "Фотографії",
"NotificationOptionVideoPlaybackStopped": "Відтворення відео зупинено",
"NotificationOptionVideoPlayback": "Розпочато відтворення відео",
"NotificationOptionUserLockedOut": "Користувача заблоковано",
"NotificationOptionTaskFailed": "Помилка запланованого завдання",
"NotificationOptionInstallationFailed": "Помилка встановлення",
"NotificationOptionCameraImageUploaded": "Фотографію завантажено",
"NotificationOptionAudioPlaybackStopped": "Відтворення аудіо зупинено",
"NotificationOptionAudioPlayback": "Розпочато відтворення аудіо",
"NotificationOptionApplicationUpdateInstalled": "Встановлено оновлення додатка",
"NotificationOptionApplicationUpdateAvailable": "Доступне оновлення додатка",
"NewVersionIsAvailable": "Для завантаження доступна нова версія Jellyfin Server.",
"NameSeasonUnknown": "Сезон Невідомий",
"NameSeasonNumber": "Сезон {0}",
"NameInstallFailed": "Не вдалося встановити {0}",
"MixedContent": "Змішаний контент",
"MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
"MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
"Inherit": "Успадкувати",
"HeaderRecordingGroups": "Групи запису"
} }

View File

@ -92,7 +92,7 @@
"HeaderRecordingGroups": "錄製組", "HeaderRecordingGroups": "錄製組",
"Inherit": "繼承", "Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕", "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。", "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道", "TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新插件", "TaskUpdatePlugins": "更新插件",

View File

@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization
} }
// Try splitting by : to handle "Germany: FSK 18" // Try splitting by : to handle "Germany: FSK 18"
var index = rating.IndexOf(':'); var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1) if (index != -1)
{ {
rating = rating.Substring(index).TrimStart(':').Trim(); rating = rating.Substring(index).TrimStart(':').Trim();
@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(culture)); throw new ArgumentNullException(nameof(culture));
} }
const string prefix = "Core"; const string Prefix = "Core";
var key = prefix + culture; var key = Prefix + culture;
return _dictionaries.GetOrAdd( return _dictionaries.GetOrAdd(
key, key,
f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
} }
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)

View File

@ -19,6 +19,7 @@ namespace Emby.Server.Implementations.Net
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
try try
{ {
retVal.EnableBroadcast = true;
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
@ -46,6 +47,7 @@ namespace Emby.Server.Implementations.Net
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
try try
{ {
retVal.EnableBroadcast = true;
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
@ -112,6 +114,7 @@ namespace Emby.Server.Implementations.Net
try try
{ {
retVal.EnableBroadcast = true;
// retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); // retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);

View File

@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
public sealed class UdpSocket : ISocket, IDisposable public sealed class UdpSocket : ISocket, IDisposable
{ {
private Socket _socket; private Socket _socket;
private int _localPort; private readonly int _localPort;
private bool _disposed = false; private bool _disposed = false;
public Socket Socket => _socket; public Socket Socket => _socket;
public IPAddress LocalIPAddress { get; }
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs() private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
{ {
SocketFlags = SocketFlags.None SocketFlags = SocketFlags.None
@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
InitReceiveSocketAsyncEventArgs(); InitReceiveSocketAsyncEventArgs();
} }
public UdpSocket(Socket socket, IPEndPoint endPoint)
{
if (socket == null)
{
throw new ArgumentNullException(nameof(socket));
}
_socket = socket;
_socket.Connect(endPoint);
InitReceiveSocketAsyncEventArgs();
}
public IPAddress LocalIPAddress { get; }
private void InitReceiveSocketAsyncEventArgs() private void InitReceiveSocketAsyncEventArgs()
{ {
var receiveBuffer = new byte[8192]; var receiveBuffer = new byte[8192];
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length); _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
_receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed; _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
var sendBuffer = new byte[8192]; var sendBuffer = new byte[8192];
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length); _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
_sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed; _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
} }
private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e) private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
{ {
var tcs = _currentReceiveTaskCompletionSource; var tcs = _currentReceiveTaskCompletionSource;
if (tcs != null) if (tcs != null)
@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
} }
} }
private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e) private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
{ {
var tcs = _currentSendTaskCompletionSource; var tcs = _currentSendTaskCompletionSource;
if (tcs != null) if (tcs != null)
@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
} }
} }
public UdpSocket(Socket socket, IPEndPoint endPoint)
{
if (socket == null)
{
throw new ArgumentNullException(nameof(socket));
}
_socket = socket;
_socket.Connect(endPoint);
InitReceiveSocketAsyncEventArgs();
}
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback) public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
} }
} }
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
if (_disposed) if (_disposed)
@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
} }
_socket?.Dispose(); _socket?.Dispose();
_receiveSocketAsyncEventArgs.Dispose();
_sendSocketAsyncEventArgs.Dispose();
_currentReceiveTaskCompletionSource?.TrySetCanceled(); _currentReceiveTaskCompletionSource?.TrySetCanceled();
_currentSendTaskCompletionSource?.TrySetCanceled(); _currentSendTaskCompletionSource?.TrySetCanceled();

View File

@ -152,7 +152,12 @@ namespace Emby.Server.Implementations.Networking
return true; return true;
} }
byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes(); if (!IPAddress.TryParse(endpoint, out var ipAddress))
{
return false;
}
byte[] octet = ipAddress.GetAddressBytes();
if ((octet[0] == 10) || if ((octet[0] == 10) ||
(octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
@ -160,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
(octet[0] == 127) || // RFC1122 (octet[0] == 127) || // RFC1122
(octet[0] == 169 && octet[1] == 254)) // RFC3927 (octet[0] == 169 && octet[1] == 254)) // RFC3927
{ {
return false; return true;
} }
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking
string excludeAddress = "[" + addressString + "]"; string excludeAddress = "[" + addressString + "]";
var subnets = LocalSubnetsFn(); var subnets = LocalSubnetsFn();
// Include any address if LAN subnets aren't specified
if (subnets.Length == 0)
{
return true;
}
// Exclude any addresses if they appear in the LAN list in [ ] // Exclude any addresses if they appear in the LAN list in [ ]
if (Array.IndexOf(subnets, excludeAddress) != -1) if (Array.IndexOf(subnets, excludeAddress) != -1)
{ {
@ -379,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
var host = uri.DnsSafeHost; var host = uri.DnsSafeHost;
_logger.LogDebug("Resolving host {0}", host); _logger.LogDebug("Resolving host {0}", host);
address = GetIpAddresses(host).Result.FirstOrDefault(); address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
if (address != null) if (address != null)
{ {

View File

@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album AlbumTitle = child.Album
}; };
var hasAlbumArtist = child as IHasAlbumArtist; if (child is IHasAlbumArtist hasAlbumArtist)
if (hasAlbumArtist != null)
{ {
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
} }
var hasArtist = child as IHasArtist; if (child is IHasArtist hasArtist)
if (hasArtist != null)
{ {
entry.TrackArtist = hasArtist.Artists.FirstOrDefault(); entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
} }
if (child.RunTimeTicks.HasValue) if (child.RunTimeTicks.HasValue)
@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album AlbumTitle = child.Album
}; };
var hasAlbumArtist = child as IHasAlbumArtist; if (child is IHasAlbumArtist hasAlbumArtist)
if (hasAlbumArtist != null)
{ {
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
} }
var hasArtist = child as IHasArtist; if (child is IHasArtist hasArtist)
if (hasArtist != null)
{ {
entry.TrackArtist = hasArtist.Artists.FirstOrDefault(); entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
} }
if (child.RunTimeTicks.HasValue) if (child.RunTimeTicks.HasValue)
@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
{ {
var playlist = new M3uPlaylist(); var playlist = new M3uPlaylist
playlist.IsExtended = true; {
IsExtended = true
};
foreach (var child in item.GetLinkedChildren()) foreach (var child in item.GetLinkedChildren())
{ {
var entry = new M3uPlaylistEntry() var entry = new M3uPlaylistEntry()
@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album Album = child.Album
}; };
var hasAlbumArtist = child as IHasAlbumArtist; if (child is IHasAlbumArtist hasAlbumArtist)
if (hasAlbumArtist != null)
{ {
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
} }
if (child.RunTimeTicks.HasValue) if (child.RunTimeTicks.HasValue)
@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album Album = child.Album
}; };
var hasAlbumArtist = child as IHasAlbumArtist; if (child is IHasAlbumArtist hasAlbumArtist)
if (hasAlbumArtist != null)
{ {
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
} }
if (child.RunTimeTicks.HasValue) if (child.RunTimeTicks.HasValue)
@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
if (!folderPath.EndsWith(Path.DirectorySeparatorChar)) if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
{ {
folderPath = folderPath + Path.DirectorySeparatorChar; folderPath += Path.DirectorySeparatorChar;
} }
var folderUri = new Uri(folderPath); var folderUri = new Uri(folderPath);
@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
return relativePath; return relativePath;
} }
private static string UnEscape(string content)
{
if (content == null)
{
return content;
}
return content.Replace("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<");
}
private static string Escape(string content)
{
if (content == null)
{
return null;
}
return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;");
}
public Folder GetPlaylistsFolder(Guid userId) public Folder GetPlaylistsFolder(Guid userId)
{ {
var typeName = "PlaylistsFolder"; const string TypeName = "PlaylistsFolder";
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ?? return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)); _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
} }
} }
} }

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IApplicationPaths _applicationPaths; private readonly IApplicationPaths _applicationPaths;
private readonly ILogger<TaskManager> _logger; private readonly ILogger<TaskManager> _logger;
private readonly IFileSystem _fileSystem;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TaskManager" /> class. /// Initializes a new instance of the <see cref="TaskManager" /> class.
@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="applicationPaths">The application paths.</param> /// <param name="applicationPaths">The application paths.</param>
/// <param name="jsonSerializer">The json serializer.</param> /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="fileSystem">The filesystem manager.</param>
public TaskManager( public TaskManager(
IApplicationPaths applicationPaths, IApplicationPaths applicationPaths,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
ILogger<TaskManager> logger, ILogger<TaskManager> logger)
IFileSystem fileSystem)
{ {
_applicationPaths = applicationPaths; _applicationPaths = applicationPaths;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_logger = logger; _logger = logger;
_fileSystem = fileSystem;
ScheduledTasks = Array.Empty<IScheduledTaskWorker>(); ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
} }

View File

@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks namespace Emby.Server.Implementations.ScheduledTasks
@ -24,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary> /// </summary>
public class ChapterImagesTask : IScheduledTask public class ChapterImagesTask : IScheduledTask
{ {
/// <summary>
/// The _logger.
/// </summary>
private readonly ILogger<ChapterImagesTask> _logger;
/// <summary> /// <summary>
/// The _library manager. /// The _library manager.
/// </summary> /// </summary>
@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary> /// </summary>
public ChapterImagesTask( public ChapterImagesTask(
ILoggerFactory loggerFactory,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IItemRepository itemRepo, IItemRepository itemRepo,
IApplicationPaths appPaths, IApplicationPaths appPaths,
@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
IFileSystem fileSystem, IFileSystem fileSystem,
ILocalizationManager localization) ILocalizationManager localization)
{ {
_logger = loggerFactory.CreateLogger<ChapterImagesTask>();
_libraryManager = libraryManager; _libraryManager = libraryManager;
_itemRepo = itemRepo; _itemRepo = itemRepo;
_appPaths = appPaths; _appPaths = appPaths;

View File

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.IO; using System.IO;
using System.Xml; using System.Xml;
using System.Xml.Serialization; using System.Xml.Serialization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.Serialization namespace Emby.Server.Implementations.Serialization
@ -53,10 +54,11 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
public void SerializeToStream(object obj, Stream stream) public void SerializeToStream(object obj, Stream stream)
{ {
using (var writer = new XmlTextWriter(stream, null)) using (var writer = new StreamWriter(stream, null, IODefaults.StreamWriterBufferSize, true))
using (var textWriter = new XmlTextWriter(writer))
{ {
writer.Formatting = Formatting.Indented; textWriter.Formatting = Formatting.Indented;
SerializeToWriter(obj, writer); SerializeToWriter(obj, textWriter);
} }
} }
@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Serialization
/// <returns>System.Object.</returns> /// <returns>System.Object.</returns>
public object DeserializeFromBytes(Type type, byte[] buffer) public object DeserializeFromBytes(Type type, byte[] buffer)
{ {
using (var stream = new MemoryStream(buffer)) using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
{ {
return DeserializeFromStream(type, stream); return DeserializeFromStream(type, stream);
} }

View File

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
{ {
if (restPath.Path[0] != '/') if (restPath.Path[0] != '/')
{ {
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName())); 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) if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
{ {
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName())); 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)) if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
var service = httpHost.CreateInstance(serviceType); var service = httpHost.CreateInstance(serviceType);
var serviceRequiresContext = service as IRequiresRequest; if (service is IRequiresRequest serviceRequiresContext)
if (serviceRequiresContext != null)
{ {
serviceRequiresContext.Request = req; serviceRequiresContext.Request = req;
} }
@ -189,5 +199,4 @@ namespace Emby.Server.Implementations.Services
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
} }
} }
} }

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