mirror of https://github.com/jellyfin/jellyfin.git
Merge remote-tracking branch 'upstream/master' into fonts
This commit is contained in:
commit
aa75755480
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
@ -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>");
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseProfiles = new ResponseProfile[] { };
|
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseProfiles = new ResponseProfile[] { };
|
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseProfiles = new ResponseProfile[] { };
|
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseProfiles = new ResponseProfile[] { };
|
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[]
|
|
||||||
{
|
|
||||||
"<",
|
|
||||||
"<",
|
|
||||||
">",
|
|
||||||
">",
|
|
||||||
"\"",
|
|
||||||
""",
|
|
||||||
"'",
|
|
||||||
"'",
|
|
||||||
"&",
|
|
||||||
"&"
|
|
||||||
};
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
|
@ -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>");
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
lock (_capabilitiesSyncLock)
|
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
|
||||||
{
|
|
||||||
if (_capabilitiesCache.TryGetValue(id, out var result))
|
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (_capabilitiesSyncLock)
|
||||||
|
{
|
||||||
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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-->
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -47,9 +48,17 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task RunAsync()
|
public Task RunAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_udpServer = new UdpServer(_logger, _appHost, _config);
|
_udpServer = new UdpServer(_logger, _appHost, _config);
|
||||||
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
|
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.");
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
|
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
|
||||||
image.Width = size.Width;
|
image.Width = size.Width;
|
||||||
image.Height = size.Height;
|
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],
|
MediaSource = episode.GetMediaSources(false)[0],
|
||||||
MediaType = DlnaProfileType.Video
|
MediaType = DlnaProfileType.Video
|
||||||
}, CancellationToken.None).GetAwaiter().GetResult();
|
},
|
||||||
|
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)
|
||||||
|
|
|
@ -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,
|
MediaSource = mediaSource,
|
||||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||||
ExtractChapters = false
|
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)
|
||||||
|
|
|
@ -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,
|
MediaSource = mediaSource,
|
||||||
ExtractChapters = false,
|
ExtractChapters = false,
|
||||||
MediaType = DlnaProfileType.Video
|
MediaType = DlnaProfileType.Video
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
},
|
||||||
|
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>
|
||||||
|
@ -891,16 +892,13 @@ namespace Emby.Server.Implementations.Library
|
||||||
protected virtual void Dispose(bool dispose)
|
protected virtual void Dispose(bool dispose)
|
||||||
{
|
{
|
||||||
if (dispose)
|
if (dispose)
|
||||||
{
|
|
||||||
lock (_disposeLock)
|
|
||||||
{
|
{
|
||||||
foreach (var key in _openStreams.Keys.ToList())
|
foreach (var key in _openStreams.Keys.ToList())
|
||||||
{
|
{
|
||||||
var task = CloseLiveStream(key);
|
CloseLiveStream(key).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
Task.WaitAll(task);
|
_liveStreamSemaphore.Dispose();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 + "\",";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +76,10 @@ 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)
|
||||||
var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
|
.ConfigureAwait(false) ?? new List<Channels>();
|
||||||
|
|
||||||
if (info.ImportFavoritesOnly)
|
if (info.ImportFavoritesOnly)
|
||||||
{
|
{
|
||||||
|
@ -87,7 +88,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
|
|
||||||
return lineup.Where(i => !i.DRM).ToList();
|
return lineup.Where(i => !i.DRM).ToList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class HdHomerunChannelInfo : ChannelInfo
|
private class HdHomerunChannelInfo : ChannelInfo
|
||||||
{
|
{
|
||||||
|
@ -132,15 +132,16 @@ 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)
|
||||||
var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(cacheKey))
|
if (!string.IsNullOrEmpty(cacheKey))
|
||||||
{
|
{
|
||||||
|
@ -152,10 +153,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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[] { ' ', '-' });
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": "تم تثبيت الملحق",
|
||||||
|
|
|
@ -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": "প্রিয় গানগুলো",
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -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}",
|
||||||
|
|
|
@ -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 שנוצרו מלפני יותר מיום.",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -57,5 +57,7 @@
|
||||||
"HeaderCameraUploads": "कॅमेरा अपलोड",
|
"HeaderCameraUploads": "कॅमेरा अपलोड",
|
||||||
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
|
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
|
||||||
"Application": "अॅप्लिकेशन",
|
"Application": "अॅप्लिकेशन",
|
||||||
"AppDeviceValues": "अॅप: {0}, यंत्र: {1}"
|
"AppDeviceValues": "अॅप: {0}, यंत्र: {1}",
|
||||||
|
"Collections": "संग्रह",
|
||||||
|
"ChapterNameValue": "धडा {0}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} ஐ இயக்குகிறது"
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,5 +67,7 @@
|
||||||
"Artists": "นักแสดง",
|
"Artists": "นักแสดง",
|
||||||
"Application": "แอปพลิเคชั่น",
|
"Application": "แอปพลิเคชั่น",
|
||||||
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
|
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
|
||||||
"Albums": "อัลบั้ม"
|
"Albums": "อัลบั้ม",
|
||||||
|
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
|
||||||
|
"ScheduledTaskFailedWithName": "{0} ล้มเหลว"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": "Групи запису"
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
"HeaderRecordingGroups": "錄製組",
|
"HeaderRecordingGroups": "錄製組",
|
||||||
"Inherit": "繼承",
|
"Inherit": "繼承",
|
||||||
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
|
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
|
||||||
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
||||||
"TaskRefreshChannels": "重新整理頻道",
|
"TaskRefreshChannels": "重新整理頻道",
|
||||||
"TaskUpdatePlugins": "更新插件",
|
"TaskUpdatePlugins": "更新插件",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Escape(string content)
|
|
||||||
{
|
|
||||||
if (content == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<");
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue