Merge remote-tracking branch 'upstream/master' into package-install-repo
This commit is contained in:
commit
b43a8fb9d3
|
@ -12,10 +12,12 @@ parameters:
|
|||
jobs:
|
||||
- job: CompatibilityCheck
|
||||
displayName: Compatibility Check
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
|
||||
pool:
|
||||
vmImage: "${{ parameters.LinuxImage }}"
|
||||
# only execute for pull requests
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
${{ each Package in parameters.Packages }}:
|
||||
|
@ -23,7 +25,7 @@ jobs:
|
|||
NugetPackageName: ${{ Package.value.NugetPackageName }}
|
||||
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
|
||||
maxParallel: 2
|
||||
dependsOn: Build
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
|
@ -34,32 +36,33 @@ jobs:
|
|||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Install ABI CompatibilityChecker tool'
|
||||
displayName: 'Install ABI CompatibilityChecker Tool'
|
||||
inputs:
|
||||
command: custom
|
||||
custom: tool
|
||||
arguments: 'update compatibilitychecker -g'
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: "Download New Assembly Build Artifact"
|
||||
displayName: 'Download New Assembly Build Artifact'
|
||||
inputs:
|
||||
source: "current"
|
||||
source: 'current'
|
||||
artifact: "$(NugetPackageName)"
|
||||
path: "$(System.ArtifactsDirectory)/new-artifacts"
|
||||
runVersion: "latest"
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: "Copy New Assembly Build Artifact"
|
||||
displayName: 'Copy New Assembly Build Artifact'
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
|
||||
contents: "**/*.dll"
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/new-release
|
||||
cleanTargetFolder: true
|
||||
overWrite: true
|
||||
flattenFolders: true
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: "Download Reference Assembly Build Artifact"
|
||||
displayName: 'Download Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
source: "specific"
|
||||
artifact: "$(NugetPackageName)"
|
||||
|
@ -70,18 +73,19 @@ jobs:
|
|||
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: "Copy Reference Assembly Build Artifact"
|
||||
displayName: 'Copy Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
|
||||
contents: "**/*.dll"
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/current-release
|
||||
cleanTargetFolder: true
|
||||
overWrite: true
|
||||
flattenFolders: true
|
||||
|
||||
# The `--warnings-only` switch will swallow the return code and not emit any errors.
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Execute ABI Compatibility Check Tool'
|
||||
enabled: false
|
||||
inputs:
|
||||
command: custom
|
||||
custom: compat
|
||||
|
|
|
@ -80,7 +80,15 @@ jobs:
|
|||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
- name: JellyfinVersion
|
||||
value: 0.0.0
|
||||
|
||||
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
|
||||
displayName: 'Push Unstable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
@ -105,9 +113,10 @@ jobs:
|
|||
containerRegistry: Docker Hub
|
||||
tags: |
|
||||
stable-$(Build.BuildNumber)-$(BuildConfiguration)
|
||||
stable-$(BuildConfiguration)
|
||||
$(JellyfinVersion)-$(BuildConfiguration)
|
||||
|
||||
- job: CollectArtifacts
|
||||
timeoutInMinutes: 10
|
||||
displayName: 'Collect Artifacts'
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
|
@ -123,19 +132,35 @@ jobs:
|
|||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'inline'
|
||||
inline: |
|
||||
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
|
||||
rm $0
|
||||
exit
|
||||
runOptions: 'commands'
|
||||
commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Update Stable Repository'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'inline'
|
||||
inline: |
|
||||
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
|
||||
rm $0
|
||||
exit
|
||||
runOptions: 'commands'
|
||||
commands: sudo -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'
|
||||
|
|
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
|
@ -6,11 +6,21 @@
|
|||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Launch (nowebclient)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
|
||||
"args": ["--nowebclient"],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
|
|
|
@ -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.Service;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
|
|
|
@ -16,5 +16,11 @@ namespace Emby.Dlna
|
|||
public string Xml { 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));
|
||||
}
|
||||
|
||||
var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
|
||||
var mediaProfile = _profile.GetVideoMediaProfile(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioBitrate,
|
||||
|
|
|
@ -122,15 +122,15 @@ namespace Emby.Dlna
|
|||
var builder = new StringBuilder();
|
||||
|
||||
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.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
|
||||
|
||||
_logger.LogInformation(builder.ToString());
|
||||
}
|
||||
|
@ -387,7 +387,7 @@ namespace Emby.Dlna
|
|||
|
||||
foreach (var name in _assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (!name.StartsWith(namespaceName))
|
||||
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -406,7 +406,7 @@ namespace Emby.Dlna
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
class InternalProfileInfo
|
||||
private class InternalProfileInfo
|
||||
{
|
||||
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\">");
|
||||
foreach (var key in stateVariables.Keys)
|
||||
{
|
||||
builder.Append("<e:property>");
|
||||
builder.Append("<" + key + ">");
|
||||
builder.Append(stateVariables[key]);
|
||||
builder.Append("</" + key + ">");
|
||||
builder.Append("</e:property>");
|
||||
builder.Append("<e:property>")
|
||||
.Append('<')
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append(stateVariables[key])
|
||||
.Append("</")
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append("</e:property>");
|
||||
}
|
||||
|
||||
builder.Append("</e:propertyset>");
|
||||
|
|
|
@ -54,11 +54,11 @@ namespace Emby.Dlna.Main
|
|||
private SsdpDevicePublisher _publisher;
|
||||
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;
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Server;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
return DescriptionXmlBuilder.Escape(value);
|
||||
return SecurityElement.Escape(value);
|
||||
}
|
||||
|
||||
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
|
|
|
@ -4,6 +4,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
|
|||
|
||||
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("<major>1</major>");
|
||||
|
@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
|
|||
|
||||
if (!EnableAbsoluteUrls)
|
||||
{
|
||||
builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
|
||||
builder.Append("<URLBase>")
|
||||
.Append(SecurityElement.Escape(_serverAddress))
|
||||
.Append("</URLBase>");
|
||||
}
|
||||
|
||||
AppendDeviceInfo(builder);
|
||||
|
@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
|
|||
|
||||
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);
|
||||
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)
|
||||
{
|
||||
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("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
|
||||
builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
|
||||
builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
|
||||
builder.Append("<friendlyName>")
|
||||
.Append(SecurityElement.Escape(GetFriendlyName()))
|
||||
.Append("</friendlyName>");
|
||||
builder.Append("<manufacturer>")
|
||||
.Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
|
||||
.Append("</manufacturer>");
|
||||
builder.Append("<manufacturerURL>")
|
||||
.Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
|
||||
.Append("</manufacturerURL>");
|
||||
|
||||
builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
|
||||
builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
|
||||
builder.Append("<modelDescription>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
|
||||
.Append("</modelDescription>");
|
||||
builder.Append("<modelName>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
|
||||
.Append("</modelName>");
|
||||
|
||||
builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
|
||||
builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
|
||||
builder.Append("<modelNumber>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
|
||||
.Append("</modelNumber>");
|
||||
builder.Append("<modelURL>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
|
||||
.Append("</modelURL>");
|
||||
|
||||
if (string.IsNullOrEmpty(_profile.SerialNumber))
|
||||
{
|
||||
builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
|
||||
builder.Append("<serialNumber>")
|
||||
.Append(SecurityElement.Escape(_serverId))
|
||||
.Append("</serialNumber>");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
|
||||
builder.Append("<serialNumber>")
|
||||
.Append(SecurityElement.Escape(_profile.SerialNumber))
|
||||
.Append("</serialNumber>");
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
|
||||
builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
|
||||
builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
|
||||
builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
|
||||
builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
|
||||
builder.Append("<mimetype>")
|
||||
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
||||
.Append("</mimetype>");
|
||||
builder.Append("<width>")
|
||||
.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>");
|
||||
}
|
||||
|
@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
|
|||
{
|
||||
builder.Append("<service>");
|
||||
|
||||
builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
|
||||
builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
|
||||
builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
|
||||
builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
|
||||
builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
|
||||
builder.Append("<serviceType>")
|
||||
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
|
||||
.Append("</serviceType>");
|
||||
builder.Append("<serviceId>")
|
||||
.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>");
|
||||
}
|
||||
|
@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
|
|||
url = _serverAddress.TrimEnd('/') + url;
|
||||
}
|
||||
|
||||
return Escape(url);
|
||||
return SecurityElement.Escape(url);
|
||||
}
|
||||
|
||||
private IEnumerable<DeviceIcon> GetIcons()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Server;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
|
@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
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>");
|
||||
|
||||
|
@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
builder.Append("<argument>");
|
||||
|
||||
builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>");
|
||||
builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>");
|
||||
builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
|
||||
.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>");
|
||||
}
|
||||
|
@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
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("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>");
|
||||
builder.Append("<name>")
|
||||
.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)
|
||||
{
|
||||
builder.Append("<allowedValueList>");
|
||||
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>");
|
||||
|
|
|
@ -448,21 +448,21 @@ namespace Emby.Drawing
|
|||
/// or
|
||||
/// filename.
|
||||
/// </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 />
|
||||
|
|
|
@ -136,8 +136,8 @@ namespace Emby.Naming.Common
|
|||
|
||||
CleanDateTimes = new[]
|
||||
{
|
||||
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{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})*",
|
||||
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
|
||||
};
|
||||
|
||||
CleanStrings = new[]
|
||||
|
@ -277,7 +277,7 @@ namespace Emby.Naming.Common
|
|||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||
// so we make sure this one gets tested first.
|
||||
// "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
|
||||
},
|
||||
|
@ -300,32 +300,32 @@ namespace Emby.Naming.Common
|
|||
// *** End Kodi Standard Naming
|
||||
|
||||
// [bar] Foo - 1 [baz]
|
||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
|
||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
|
||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||
{
|
||||
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
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
// "01.avi"
|
||||
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
|
||||
new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
|
||||
{
|
||||
IsOptimistic = true,
|
||||
IsNamed = true
|
||||
|
@ -335,34 +335,34 @@ namespace Emby.Naming.Common
|
|||
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
|
||||
|
||||
// "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,
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// "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,
|
||||
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"
|
||||
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
|
||||
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
||||
{
|
||||
IsOptimistic = true,
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// "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,
|
||||
IsNamed = true
|
||||
},
|
||||
// "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,
|
||||
IsNamed = true
|
||||
|
@ -625,17 +625,17 @@ namespace Emby.Naming.Common
|
|||
AudioBookPartsExpressions = new[]
|
||||
{
|
||||
// Detect specified chapters, like CH 01
|
||||
@"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
|
||||
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
|
||||
// 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>\d+)",
|
||||
"^(?<chapter>[0-9]+)",
|
||||
// Part if often ending of filename
|
||||
@"(?<part>\d+)$",
|
||||
"(?<part>[0-9]+)$",
|
||||
// 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.
|
||||
@"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
|
||||
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
|
||||
};
|
||||
|
||||
var extensions = VideoFileExtensions.ToList();
|
||||
|
@ -675,16 +675,16 @@ namespace Emby.Naming.Common
|
|||
|
||||
MultipleEpisodeExpressions = new string[]
|
||||
{
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{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]?\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]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{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](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<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>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{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]?[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]?[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]?[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>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{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)
|
||||
{
|
||||
IsNamed = true
|
||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Naming.TV
|
|||
|
||||
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))
|
||||
{
|
||||
|
|
|
@ -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.IO;
|
||||
using System.Linq;
|
||||
|
@ -22,7 +24,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||
{
|
||||
object configuration;
|
||||
|
||||
byte[] buffer = null;
|
||||
byte[]? buffer = null;
|
||||
|
||||
// Use try/catch to avoid the extra file system lookup using File.Exists
|
||||
try
|
||||
|
@ -36,19 +38,23 @@ namespace Emby.Server.Implementations.AppBase
|
|||
configuration = Activator.CreateInstance(type);
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
||||
xmlSerializer.SerializeToStream(configuration, stream);
|
||||
|
||||
// Take the object we just got and serialize it back to bytes
|
||||
var newBytes = stream.ToArray();
|
||||
byte[] newBytes = stream.GetBuffer();
|
||||
int newBytesLen = (int)stream.Length;
|
||||
|
||||
// If the file didn't exist before, or if something has changed, re-save
|
||||
if (buffer == null || !buffer.SequenceEqual(newBytes))
|
||||
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
// Save it after load in case we got new items
|
||||
File.WriteAllBytes(path, newBytes);
|
||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
fs.Write(newBytes, 0, newBytesLen);
|
||||
}
|
||||
}
|
||||
|
||||
return configuration;
|
||||
|
|
|
@ -4,7 +4,6 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
@ -46,7 +45,7 @@ using Emby.Server.Implementations.Session;
|
|||
using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using MediaBrowser.Api;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
|
@ -97,7 +96,6 @@ using MediaBrowser.Providers.Chapters;
|
|||
using MediaBrowser.Providers.Manager;
|
||||
using MediaBrowser.Providers.Plugins.TheTvdb;
|
||||
using MediaBrowser.Providers.Subtitles;
|
||||
using MediaBrowser.WebDashboard.Api;
|
||||
using MediaBrowser.XbmcMetadata.Providers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -192,7 +190,7 @@ namespace Emby.Server.Implementations
|
|||
/// Gets or sets the application paths.
|
||||
/// </summary>
|
||||
/// <value>The application paths.</value>
|
||||
protected ServerApplicationPaths ApplicationPaths { get; set; }
|
||||
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets all concrete types.
|
||||
|
@ -236,7 +234,7 @@ namespace Emby.Server.Implementations
|
|||
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
|
||||
/// </summary>
|
||||
public ApplicationHost(
|
||||
ServerApplicationPaths applicationPaths,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupOptions options,
|
||||
IFileSystem fileSystem,
|
||||
|
@ -554,8 +552,6 @@ namespace Emby.Server.Implementations
|
|||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||
|
@ -634,6 +630,8 @@ namespace Emby.Server.Implementations
|
|||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -650,7 +648,6 @@ namespace Emby.Server.Implementations
|
|||
_httpServer = Resolve<IHttpServer>();
|
||||
_httpClient = Resolve<IHttpClient>();
|
||||
|
||||
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
|
||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||
|
||||
SetStaticProperties();
|
||||
|
@ -795,7 +792,6 @@ namespace Emby.Server.Implementations
|
|||
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
|
||||
|
||||
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
||||
Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
|
||||
|
||||
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
|
||||
}
|
||||
|
@ -1035,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
|
||||
yield return typeof(SystemInfo).Assembly;
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Browser
|
||||
|
@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
|
|||
/// <param name="appHost">The app host.</param>
|
||||
public static void OpenSwaggerPage(IServerApplicationHost appHost)
|
||||
{
|
||||
TryOpenUrl(appHost, "/swagger/index.html");
|
||||
TryOpenUrl(appHost, "/api-docs/swagger");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -7,6 +6,7 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
|
@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
||||
|
@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
|
||||
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
|
||||
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
|
@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
/// <param name="userDataManager">The user data manager.</param>
|
||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
||||
/// <param name="providerManager">The provider manager.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
public ChannelManager(
|
||||
IUserManager userManager,
|
||||
IDtoService dtoService,
|
||||
|
@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
|
|||
IFileSystem fileSystem,
|
||||
IUserDataManager userDataManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IProviderManager providerManager)
|
||||
IProviderManager providerManager,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
|
@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
_userDataManager = userDataManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_providerManager = providerManager;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.Item2;
|
||||
}
|
||||
return cachedInfo;
|
||||
}
|
||||
|
||||
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var list = mediaInfo.ToList();
|
||||
|
||||
var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
|
||||
_channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
|
||||
_memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||
|
||||
namespace Emby.Server.Implementations
|
||||
|
@ -19,7 +18,8 @@ namespace Emby.Server.Implementations
|
|||
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
|
||||
{ FfmpegProbeSizeKey, "1G" },
|
||||
{ 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.Threading;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller;
|
||||
|
@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
|
|||
"OwnerId"
|
||||
};
|
||||
|
||||
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
|
||||
|
||||
private static readonly string[] _mediaStreamSaveColumns =
|
||||
{
|
||||
"ItemId",
|
||||
|
@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
|
|||
"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 =
|
||||
{
|
||||
"ItemId",
|
||||
|
@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
|
|||
"MIMEType"
|
||||
};
|
||||
|
||||
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
|
||||
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
|
||||
|
||||
private static readonly string _mediaAttachmentInsertPrefix;
|
||||
|
||||
private static string GetSaveItemCommandText()
|
||||
{
|
||||
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"
|
||||
};
|
||||
|
||||
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 + ")";
|
||||
}
|
||||
private const string SaveItemCommandText =
|
||||
@"replace into TypedBaseItems
|
||||
(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)";
|
||||
|
||||
/// <summary>
|
||||
/// Save a standard item in the repo.
|
||||
|
@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
var statements = PrepareAll(db, new string[]
|
||||
{
|
||||
GetSaveItemCommandText(),
|
||||
SaveItemCommandText,
|
||||
"delete from AncestorIds where ItemId=@ItemId"
|
||||
}).ToList();
|
||||
|
||||
|
@ -1056,7 +978,10 @@ namespace Emby.Server.Implementations.Data
|
|||
continue;
|
||||
}
|
||||
|
||||
str.Append($"{i.Key}={i.Value}|");
|
||||
str.Append(i.Key)
|
||||
.Append('=')
|
||||
.Append(i.Value)
|
||||
.Append('|');
|
||||
}
|
||||
|
||||
if (str.Length == 0)
|
||||
|
@ -1110,7 +1035,8 @@ namespace Emby.Server.Implementations.Data
|
|||
continue;
|
||||
}
|
||||
|
||||
str.Append(ToValueString(i) + "|");
|
||||
AppendItemImageInfo(str, i);
|
||||
str.Append('|');
|
||||
}
|
||||
|
||||
str.Length -= 1; // Remove last |
|
||||
|
@ -1144,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
|
|||
item.ImageInfos = list.ToArray();
|
||||
}
|
||||
|
||||
public string ToValueString(ItemImageInfo image)
|
||||
public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
|
||||
{
|
||||
const string Delimeter = "*";
|
||||
const char Delimiter = '*';
|
||||
|
||||
var path = image.Path ?? string.Empty;
|
||||
var hash = image.BlurHash ?? string.Empty;
|
||||
|
||||
return GetPathToSave(path) +
|
||||
Delimeter +
|
||||
image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
image.Type +
|
||||
Delimeter +
|
||||
image.Width.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
image.Height.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
// Replace delimiters with other characters.
|
||||
// This can be removed when we migrate to a proper DB.
|
||||
hash.Replace('*', '/').Replace('|', '\\');
|
||||
bldr.Append(GetPathToSave(path))
|
||||
.Append(Delimiter)
|
||||
.Append(image.DateModified.Ticks)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Type)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Width)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Height)
|
||||
.Append(Delimiter)
|
||||
// Replace delimiters with other characters.
|
||||
// This can be removed when we migrate to a proper DB.
|
||||
.Append(hash.Replace('*', '/').Replace('|', '\\'));
|
||||
}
|
||||
|
||||
public ItemImageInfo ItemImageInfoFromValueString(string value)
|
||||
|
@ -1225,7 +1151,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
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);
|
||||
|
||||
|
@ -2471,7 +2397,7 @@ namespace Emby.Server.Implementations.Data
|
|||
var item = query.SimilarTo;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("(");
|
||||
builder.Append('(');
|
||||
|
||||
if (string.IsNullOrEmpty(item.OfficialRating))
|
||||
{
|
||||
|
@ -2509,7 +2435,7 @@ namespace Emby.Server.Implementations.Data
|
|||
if (!string.IsNullOrEmpty(query.SearchTerm))
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("(");
|
||||
builder.Append('(');
|
||||
|
||||
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)
|
||||
{
|
||||
if (buffer.IndexOf('\u2013') > -1)
|
||||
if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2013', '-'); // en dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2014') > -1)
|
||||
if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2014', '-'); // em dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2015') > -1)
|
||||
if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2018') > -1)
|
||||
if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201a') > -1)
|
||||
if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201c') > -1)
|
||||
if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201e') > -1)
|
||||
if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2033') > -1)
|
||||
if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u0060') > -1)
|
||||
if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u00B4') > -1)
|
||||
if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
||||
}
|
||||
|
@ -2999,7 +2925,6 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
|
||||
var statements = PrepareAll(db, statementTexts).ToList();
|
||||
|
||||
if (!isReturningZeroItems)
|
||||
|
@ -4669,8 +4594,12 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
if (query.BlockUnratedItems.Length > 1)
|
||||
{
|
||||
var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||
whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
|
||||
var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||
whereClauses.Add(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
|
||||
inClause));
|
||||
}
|
||||
|
||||
if (query.ExcludeInheritedTags.Length > 0)
|
||||
|
@ -4679,7 +4608,7 @@ namespace Emby.Server.Implementations.Data
|
|||
if (statement == null)
|
||||
{
|
||||
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)");
|
||||
}
|
||||
else
|
||||
|
@ -5238,7 +5167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
{
|
||||
if (i > 0)
|
||||
{
|
||||
insertText.Append(",");
|
||||
insertText.Append(',');
|
||||
}
|
||||
|
||||
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;
|
||||
var startIndex = 0;
|
||||
|
||||
const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
|
||||
var insertText = new StringBuilder(StartInsertText);
|
||||
while (startIndex < values.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
|
||||
|
||||
var endIndex = Math.Min(values.Count, startIndex + Limit);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
|
@ -5778,6 +5707,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
}
|
||||
|
||||
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 listIndex = 0;
|
||||
|
||||
const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
|
||||
var insertText = new StringBuilder(StartInsertText);
|
||||
while (startIndex < people.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
|
||||
|
||||
var endIndex = Math.Min(people.Count, startIndex + Limit);
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
|
@ -5852,6 +5782,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
var cmdText = "select "
|
||||
+ string.Join(",", _mediaStreamSaveColumns)
|
||||
+ " from mediastreams where"
|
||||
+ " ItemId=@ItemId";
|
||||
var cmdText = _mediaStreamSaveColumnsSelectQuery;
|
||||
|
||||
if (query.Type.HasValue)
|
||||
{
|
||||
|
@ -5970,18 +5898,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
const int Limit = 10;
|
||||
var startIndex = 0;
|
||||
|
||||
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
|
||||
while (startIndex < streams.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("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);
|
||||
|
||||
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;
|
||||
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));
|
||||
}
|
||||
|
||||
var cmdText = "select "
|
||||
+ string.Join(",", _mediaAttachmentSaveColumns)
|
||||
+ " from mediaattachments where"
|
||||
+ " ItemId=@ItemId";
|
||||
var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
|
||||
|
||||
if (query.Index.HasValue)
|
||||
{
|
||||
|
@ -6318,10 +6235,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
{
|
||||
const int InsertAtOnce = 10;
|
||||
|
||||
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
|
||||
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
|
||||
{
|
||||
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
|
||||
|
||||
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
|
@ -6331,7 +6247,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
|
||||
{
|
||||
insertText.Append("@" + column + index + ",");
|
||||
insertText.Append('@')
|
||||
.Append(column)
|
||||
.Append(index)
|
||||
.Append(',');
|
||||
}
|
||||
|
||||
insertText.Length -= 1;
|
||||
|
@ -6364,6 +6283,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
statement.Reset();
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
insertText.Length = _mediaAttachmentInsertPrefix.Length;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ using System.Collections.Generic;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
|
@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
|
|||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
public class DeviceManager : IDeviceManager
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
|
||||
private readonly object _capabilitiesSyncLock = new object();
|
||||
|
||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||
|
@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
|
|||
IAuthenticationRepository authRepo,
|
||||
IJsonSerializer json,
|
||||
IUserManager userManager,
|
||||
IServerConfigurationManager config)
|
||||
IServerConfigurationManager config,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_json = json;
|
||||
_userManager = userManager;
|
||||
_config = config;
|
||||
_memoryCache = memoryCache;
|
||||
_authRepo = authRepo;
|
||||
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
||||
|
@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
|
|||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
_capabilitiesCache[deviceId] = capabilities;
|
||||
|
||||
_memoryCache.Set(deviceId, capabilities);
|
||||
_json.SerializeToFile(capabilities, path);
|
||||
}
|
||||
}
|
||||
|
@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
|
|||
|
||||
public ClientCapabilities GetCapabilities(string id)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
if (_capabilitiesCache.TryGetValue(id, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
||||
try
|
||||
{
|
||||
|
|
|
@ -73,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
|
|||
_livetvManagerFactory = livetvManagerFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a BaseItem to a DTOBaseItem.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="fields">The fields.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <returns>Task{DtoBaseItem}.</returns>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null)
|
||||
{
|
||||
var options = new DtoOptions
|
||||
{
|
||||
Fields = fields
|
||||
};
|
||||
|
||||
return GetBaseItemDto(item, options, user, owner);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
|
@ -443,17 +424,6 @@ namespace Emby.Server.Implementations.Dto
|
|||
return folder.GetChildCount(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets client-side Id of a server-side BaseItem.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public string GetDtoId(BaseItem item)
|
||||
{
|
||||
return item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static void SetBookProperties(BaseItemDto dto, Book item)
|
||||
{
|
||||
dto.SeriesName = item.SeriesName;
|
||||
|
@ -484,6 +454,11 @@ namespace Emby.Server.Implementations.Dto
|
|||
}
|
||||
}
|
||||
|
||||
private string GetDtoId(BaseItem item)
|
||||
{
|
||||
return item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Album))
|
||||
|
@ -513,19 +488,6 @@ namespace Emby.Server.Implementations.Dto
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
private string GetImageCacheTag(BaseItem item, ImageType type)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _imageProcessor.GetImageCacheTag(item, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting {type} image info", type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -13,10 +13,8 @@
|
|||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
|
||||
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
|
||||
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
||||
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
||||
|
@ -25,7 +23,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IPNetwork2" Version="2.5.211" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
|
||||
<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.Server.Kestrel" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.2" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.25.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
|
||||
<PackageReference Include="sharpcompress" Version="0.26.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
|
||||
</ItemGroup>
|
||||
|
@ -54,7 +52,7 @@
|
|||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
|
|
@ -23,10 +23,12 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
public class LibraryChangedNotifier : IServerEntryPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger<LibraryChangedNotifier> _logger;
|
||||
|
@ -38,23 +40,10 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
private readonly List<Folder> _foldersAddedTo = new List<Folder>();
|
||||
private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
|
||||
|
||||
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the library update timer.
|
||||
/// </summary>
|
||||
/// <value>The library update timer.</value>
|
||||
private Timer LibraryUpdateTimer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
|
||||
|
||||
public LibraryChangedNotifier(
|
||||
ILibraryManager libraryManager,
|
||||
|
@ -70,22 +59,26 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the library update timer.
|
||||
/// </summary>
|
||||
/// <value>The library update timer.</value>
|
||||
private Timer LibraryUpdateTimer { get; set; }
|
||||
|
||||
public Task RunAsync()
|
||||
{
|
||||
_libraryManager.ItemAdded += libraryManager_ItemAdded;
|
||||
_libraryManager.ItemUpdated += libraryManager_ItemUpdated;
|
||||
_libraryManager.ItemRemoved += libraryManager_ItemRemoved;
|
||||
_libraryManager.ItemAdded += OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved += OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
|
||||
_providerManager.RefreshStarted += _providerManager_RefreshStarted;
|
||||
_providerManager.RefreshProgress += _providerManager_RefreshProgress;
|
||||
_providerManager.RefreshCompleted += OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted += OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress += OnProviderRefreshProgress;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
|
||||
|
||||
private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
{
|
||||
var item = e.Argument.Item1;
|
||||
|
||||
|
@ -122,9 +115,11 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
var collectionFolderDict = new Dictionary<string, string>();
|
||||
collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
|
||||
var collectionFolderDict = new Dictionary<string, string>
|
||||
{
|
||||
["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -136,21 +131,19 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
}
|
||||
}
|
||||
|
||||
private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
|
||||
private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
}
|
||||
|
||||
private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
}
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
{
|
||||
var folder = item as Folder;
|
||||
|
||||
if (folder == null)
|
||||
if (!(item is Folder folder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -183,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
|
@ -205,8 +198,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
var parent = e.Item.GetParent() as Folder;
|
||||
if (parent != null)
|
||||
if (e.Item.GetParent() is Folder parent)
|
||||
{
|
||||
_foldersAddedTo.Add(parent);
|
||||
}
|
||||
|
@ -220,7 +212,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
|
@ -231,8 +223,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (LibraryUpdateTimer == null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -248,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
|
@ -259,16 +250,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (LibraryUpdateTimer == null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
var parent = e.Parent as Folder;
|
||||
if (parent != null)
|
||||
if (e.Parent is Folder parent)
|
||||
{
|
||||
_foldersRemovedFrom.Add(parent);
|
||||
}
|
||||
|
@ -486,13 +475,13 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
LibraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
_libraryManager.ItemAdded -= libraryManager_ItemAdded;
|
||||
_libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
|
||||
_libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
|
||||
_libraryManager.ItemAdded -= OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
|
||||
_providerManager.RefreshStarted -= _providerManager_RefreshStarted;
|
||||
_providerManager.RefreshProgress -= _providerManager_RefreshProgress;
|
||||
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The _options.
|
||||
|
@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
}
|
||||
|
||||
_streamHelper = streamHelper;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
Path = path;
|
||||
_logger = logger;
|
||||
|
|
|
@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpRes.StatusCode = 200;
|
||||
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
|
@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
var handler = GetServiceHandler(httpReq);
|
||||
if (handler != null)
|
||||
{
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -567,7 +567,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
|
||||
|
||||
var connection = new WebSocketConnection(
|
||||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.Connection.RemoteIpAddress,
|
||||
|
|
|
@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
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";
|
||||
}
|
||||
|
@ -326,7 +326,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
return GetHttpResult(request, ms, contentType, true, responseHeaders);
|
||||
}
|
||||
|
||||
private IHasHeaders GetCompressedResult(byte[] content,
|
||||
private IHasHeaders GetCompressedResult(
|
||||
byte[] content,
|
||||
string requestedCompressionType,
|
||||
IDictionary<string, string> responseHeaders,
|
||||
bool isHeadRequest,
|
||||
|
|
|
@ -13,35 +13,31 @@ using MediaBrowser.Controller.Security;
|
|||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
private readonly IAuthorizationContext _authorizationContext;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
||||
public AuthService(
|
||||
ILogger<AuthService> logger,
|
||||
IAuthorizationContext authorizationContext,
|
||||
IServerConfigurationManager config,
|
||||
ISessionManager sessionManager,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_authorizationContext = authorizationContext;
|
||||
_config = config;
|
||||
_sessionManager = sessionManager;
|
||||
_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)
|
||||
|
@ -67,17 +63,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
return auth;
|
||||
}
|
||||
|
||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
// This code is executed before the service
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
|
||||
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
||||
if (!IsExemptFromAuthenticationToken(authAttributes, request))
|
||||
{
|
||||
ValidateSecurityToken(request, auth.Token);
|
||||
}
|
||||
|
||||
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
|
||||
if (authAttributes.AllowLocalOnly && !request.IsLocal)
|
||||
{
|
||||
throw new SecurityException("Operation not found.");
|
||||
}
|
||||
|
@ -91,14 +87,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
|
||||
if (user != null)
|
||||
{
|
||||
ValidateUserAccess(user, request, authAttribtues, auth);
|
||||
ValidateUserAccess(user, request, authAttributes);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -122,8 +118,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
private void ValidateUserAccess(
|
||||
User user,
|
||||
IRequest request,
|
||||
IAuthenticationAttributes authAttributes,
|
||||
AuthorizationInfo auth)
|
||||
IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||
{
|
||||
|
@ -162,6 +157,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
return true;
|
||||
}
|
||||
|
||||
if (authAttribtues.IgnoreLegacyAuth)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -241,16 +241,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
{
|
||||
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"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = queryString["ApiKey"];
|
||||
}
|
||||
|
||||
// TODO deprecate this query parameter.
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = queryString["api_key"];
|
||||
|
@ -276,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
|
||||
private static string NormalizeValue(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return WebUtility.HtmlEncode(value);
|
||||
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,13 +95,13 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
if (bytes != null)
|
||||
{
|
||||
await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
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>
|
||||
/// Class WebSocketConnection.
|
||||
/// </summary>
|
||||
public class WebSocketConnection : IWebSocketConnection
|
||||
public class WebSocketConnection : IWebSocketConnection, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
|
@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
Memory<byte> memory = writer.GetMemory(512);
|
||||
try
|
||||
{
|
||||
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
|
||||
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
|
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
writer.Advance(bytesRead);
|
||||
|
||||
// Make the data available to the PipeReader
|
||||
FlushResult flushResult = await writer.FlushAsync();
|
||||
FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
|
||||
if (flushResult.IsCompleted)
|
||||
{
|
||||
// The PipeReader stopped reading
|
||||
|
@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
|
||||
{
|
||||
await SendKeepAliveResponse();
|
||||
await SendKeepAliveResponse().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
|
|||
private readonly List<string> _affectedPaths = new List<string>();
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private bool _disposed;
|
||||
|
||||
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()
|
||||
{
|
||||
_disposed = true;
|
||||
DisposeTimer();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
|
|||
if (info is FileInfo fileInfo)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
|
|||
/// </summary>
|
||||
public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
||||
public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
|
||||
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Emby.Server.Implementations.Images;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Server.Implementations.Images;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
|
|||
if (parent != null)
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
return true;
|
||||
|
|
|
@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
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 string OriginalStreamId { get; set; }
|
||||
|
@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
public MediaSourceInfo MediaSource { get; set; }
|
||||
|
||||
public string UniqueId { get; private set; }
|
||||
|
||||
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 string UniqueId { get; }
|
||||
|
||||
public Task Close()
|
||||
{
|
||||
|
|
|
@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
"**/small.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
|
||||
"**/metadata/**",
|
||||
|
@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
|
|||
"**/.grab/**",
|
||||
"**/.grab",
|
||||
|
||||
// Unix hidden files and directories
|
||||
"**/.*/**",
|
||||
// Unix hidden files
|
||||
"**/.*",
|
||||
|
||||
// Mac - if you ever remove the above.
|
||||
// "**/._*",
|
||||
// "**/.DS_Store",
|
||||
|
||||
// thumbs.db
|
||||
"**/thumbs.db",
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
|
|||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Providers.MediaInfo;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
using Person = MediaBrowser.Controller.Entities.Person;
|
||||
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
|
||||
using VideoResolver = Emby.Naming.Video.VideoResolver;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
@ -60,7 +59,10 @@ namespace Emby.Server.Implementations.Library
|
|||
/// </summary>
|
||||
public class LibraryManager : ILibraryManager
|
||||
{
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
|
||||
private readonly ILogger<LibraryManager> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
|
@ -72,12 +74,118 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
|
||||
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 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 IProviderManager ProviderManager => _providerManagerFactory.Value;
|
||||
|
@ -116,75 +224,8 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <value>The comparers.</value>
|
||||
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; }
|
||||
|
||||
/// <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>
|
||||
/// Adds the parts.
|
||||
/// </summary>
|
||||
|
@ -208,41 +249,6 @@ namespace Emby.Server.Implementations.Library
|
|||
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>
|
||||
/// Records the configuration values.
|
||||
/// </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)
|
||||
|
@ -341,7 +347,7 @@ namespace Emby.Server.Implementations.Library
|
|||
if (item is LiveTvProgram)
|
||||
{
|
||||
_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.Name ?? "Unknown name",
|
||||
item.Path ?? string.Empty,
|
||||
|
@ -350,7 +356,7 @@ namespace Emby.Server.Implementations.Library
|
|||
else
|
||||
{
|
||||
_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.Name ?? "Unknown name",
|
||||
item.Path ?? string.Empty,
|
||||
|
@ -368,7 +374,12 @@ namespace Emby.Server.Implementations.Library
|
|||
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
|
||||
{
|
||||
|
@ -392,7 +403,13 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
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)
|
||||
{
|
||||
Directory.Delete(fileSystemInfo.FullName, true);
|
||||
|
@ -430,7 +447,7 @@ namespace Emby.Server.Implementations.Library
|
|||
_itemRepository.DeleteItem(child.Id);
|
||||
}
|
||||
|
||||
_libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
|
||||
_memoryCache.Remove(item.Id);
|
||||
|
||||
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
|
||||
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
|
||||
.TrimStart(new[] { '/', '\\' })
|
||||
.Replace("/", "\\");
|
||||
.TrimStart('/', '\\')
|
||||
.Replace('/', '\\');
|
||||
}
|
||||
|
||||
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
|
||||
|
@ -764,14 +781,11 @@ namespace Emby.Server.Implementations.Library
|
|||
return rootFolder;
|
||||
}
|
||||
|
||||
private volatile UserRootFolder _userRootFolder;
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
public Folder GetUserRootFolder()
|
||||
{
|
||||
if (_userRootFolder == null)
|
||||
{
|
||||
lock (_syncLock)
|
||||
lock (_userRootFolderSyncLock)
|
||||
{
|
||||
if (_userRootFolder == null)
|
||||
{
|
||||
|
@ -1234,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
|
|||
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;
|
||||
}
|
||||
|
@ -1321,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
Items = _itemRepository.GetItemList(query).ToArray()
|
||||
Items = _itemRepository.GetItemList(query)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1452,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
|
|||
return _itemRepository.GetItems(query);
|
||||
}
|
||||
|
||||
var list = _itemRepository.GetItemList(query);
|
||||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
Items = list
|
||||
Items = _itemRepository.GetItemList(query)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1579,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
|
|||
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
|
||||
{
|
||||
var tasks = IntroProviders
|
||||
.OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
|
||||
.Take(1)
|
||||
.Select(i => GetIntros(i, item, user));
|
||||
|
||||
|
@ -1865,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
|
@ -1934,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <summary>
|
||||
/// Updates the item.
|
||||
/// </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
|
||||
var itemsList = items.ToList();
|
||||
|
||||
foreach (var item in itemsList)
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
|
@ -1951,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
|
|||
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(itemsList, cancellationToken);
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
if (ItemUpdated != null)
|
||||
{
|
||||
foreach (var item in itemsList)
|
||||
foreach (var item in items)
|
||||
{
|
||||
// With the live tv guide this just creates too much noise
|
||||
if (item.SourceType != SourceType.Library)
|
||||
|
@ -2178,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
|
|||
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
||||
}
|
||||
|
||||
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
||||
|
||||
public UserView GetNamedView(
|
||||
User user,
|
||||
string name,
|
||||
|
@ -2477,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
||||
|
||||
var episodeInfo = episode.IsFileProtocol ?
|
||||
resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
|
||||
new Naming.TV.EpisodeInfo();
|
||||
|
||||
if (episodeInfo == null)
|
||||
{
|
||||
episodeInfo = new Naming.TV.EpisodeInfo();
|
||||
}
|
||||
var episodeInfo = episode.IsFileProtocol
|
||||
? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
|
||||
: new Naming.TV.EpisodeInfo();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -2492,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
|
|||
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Read from metadata
|
||||
var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
MediaSource = episode.GetMediaSources(false)[0],
|
||||
MediaType = DlnaProfileType.Video
|
||||
}, CancellationToken.None).GetAwaiter().GetResult();
|
||||
var mediaInfo = _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = episode.GetMediaSources(false)[0],
|
||||
MediaType = DlnaProfileType.Video
|
||||
},
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (mediaInfo.ParentIndexNumber > 0)
|
||||
{
|
||||
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
|
||||
|
@ -2654,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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)
|
||||
{
|
||||
|
@ -2671,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
|
|||
.Select(video =>
|
||||
{
|
||||
// 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 (dbItem != null)
|
||||
if (GetItemById(video.Id) is Trailer dbItem)
|
||||
{
|
||||
video = dbItem;
|
||||
}
|
||||
|
@ -3000,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)
|
||||
{
|
||||
AddMediaPathInternal(virtualFolderName, pathInfo, true);
|
||||
|
@ -3041,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
|
|||
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 virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
|
@ -3084,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
|
|||
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 virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
|
@ -3220,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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)
|
||||
|
|
|
@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private IJsonSerializer _json;
|
||||
private IApplicationPaths _appPaths;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
|
||||
{
|
||||
|
@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
mediaSource.AnalyzeDurationMs = 3000;
|
||||
|
||||
mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
mediaInfo = await _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cacheFilePath != null)
|
||||
{
|
||||
|
@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
|
|||
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)
|
||||
{
|
||||
|
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
|
|||
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.BitRate.HasValue)
|
||||
|
|
|
@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
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 IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
@ -40,6 +43,9 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly ILocalizationManager _localizationManager;
|
||||
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;
|
||||
|
||||
public MediaSourceManager(
|
||||
|
@ -368,7 +374,6 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
|
||||
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
|
||||
|
||||
|
@ -451,9 +456,6 @@ namespace Emby.Server.Implementations.Library
|
|||
.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)
|
||||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
@ -619,12 +621,14 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (liveStreamInfo is IDirectStreamProvider)
|
||||
{
|
||||
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
ExtractChapters = false,
|
||||
MediaType = DlnaProfileType.Video
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
var info = await _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
ExtractChapters = false,
|
||||
MediaType = DlnaProfileType.Video
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
mediaSource.MediaStreams = info.MediaStreams;
|
||||
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 const char LiveStreamIdDelimeter = '_';
|
||||
|
||||
private Tuple<IMediaSourceProvider, string> GetProvider(string key)
|
||||
private (IMediaSourceProvider, string) GetProvider(string 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 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);
|
||||
|
||||
return new Tuple<IMediaSourceProvider, string>(provider, keyId);
|
||||
return (provider, keyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
|
|||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private readonly object _disposeLock = new object();
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
|
@ -892,15 +893,12 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
if (dispose)
|
||||
{
|
||||
lock (_disposeLock)
|
||||
foreach (var key in _openStreams.Keys.ToList())
|
||||
{
|
||||
foreach (var key in _openStreams.Keys.ToList())
|
||||
{
|
||||
var task = CloseLiveStream(key);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
CloseLiveStream(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_liveStreamSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System.Globalization;
|
||||
using Emby.Naming.TV;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
/// </summary>
|
||||
public class SeasonResolver : FolderResolver<Season>
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILogger<SeasonResolver> _logger;
|
||||
|
@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The config.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="localization">The localization.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SeasonResolver(
|
||||
IServerConfigurationManager config,
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localization,
|
||||
ILogger<SeasonResolver> logger)
|
||||
{
|
||||
_config = config;
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
_logger = logger;
|
||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Search;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
public class SearchEngine : ISearchEngine
|
||||
{
|
||||
private readonly ILogger<SearchEngine> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
|
||||
public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
|
|||
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
|
||||
{
|
||||
User user = null;
|
||||
|
||||
if (query.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
}
|
||||
else
|
||||
if (query.UserId != Guid.Empty)
|
||||
{
|
||||
user = _userManager.GetUserById(query.UserId);
|
||||
}
|
||||
|
@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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)
|
||||
{
|
||||
results = results.Take(query.Limit.Value).ToList();
|
||||
results = results.GetRange(0, query.Limit.Value);
|
||||
}
|
||||
|
||||
return new QueryResult<SearchHintInfo>
|
||||
{
|
||||
TotalRecordCount = totalRecordCount,
|
||||
|
||||
Items = results.ToArray()
|
||||
Items = results
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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();
|
||||
|
|
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Book = MediaBrowser.Controller.Entities.Book;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly ConcurrentDictionary<string, UserItemData> _userData =
|
||||
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ILogger<UserDataManager> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataRepository _repository;
|
||||
|
||||
public UserDataManager(
|
||||
ILogger<UserDataManager> logger,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager,
|
||||
IUserDataRepository repository)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_userManager = userManager;
|
||||
_repository = repository;
|
||||
|
|
|
@ -12,6 +12,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
|
|
|
@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
|
||||
ListingsProviderInfo info,
|
||||
List<string> programIds,
|
||||
CancellationToken cancellationToken)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
|
@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
{
|
||||
var imageId = i.Substring(0, 10);
|
||||
|
||||
if (!imageIdString.Contains(imageId))
|
||||
if (!imageIdString.Contains(imageId, StringComparison.Ordinal))
|
||||
{
|
||||
imageIdString += "\"" + imageId + "\",";
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
|
@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
|
@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly LiveTvDtoService _tvDtoService;
|
||||
|
@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
ILibraryManager libraryManager,
|
||||
ITaskManager taskManager,
|
||||
ILocalizationManager localization,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IFileSystem fileSystem,
|
||||
IChannelManager channelManager,
|
||||
LiveTvDtoService liveTvDtoService)
|
||||
|
@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
_libraryManager = libraryManager;
|
||||
_taskManager = taskManager;
|
||||
_localization = localization;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_dtoService = dtoService;
|
||||
_userDataManager = userDataManager;
|
||||
|
@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
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));
|
||||
|
||||
|
@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
// Hack to make the object a pure ListingsProviderInfo instead of an 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));
|
||||
|
||||
|
|
|
@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
public class LiveTvMediaSourceProvider : IMediaSourceProvider
|
||||
{
|
||||
// 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 string StreamIdDelimeterString = "_";
|
||||
private const char StreamIdDelimiter = '_';
|
||||
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
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)
|
||||
|
@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
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
|
||||
|
@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
/// <inheritdoc />
|
||||
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 info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
@ -14,7 +14,7 @@ using MediaBrowser.Controller.LiveTv;
|
|||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
@ -23,17 +23,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
protected readonly IServerConfigurationManager Config;
|
||||
protected readonly ILogger<BaseTunerHost> Logger;
|
||||
protected IJsonSerializer JsonSerializer;
|
||||
protected readonly IFileSystem FileSystem;
|
||||
|
||||
private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
|
||||
new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
|
||||
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
|
||||
{
|
||||
Config = config;
|
||||
Logger = logger;
|
||||
JsonSerializer = jsonSerializer;
|
||||
_memoryCache = memoryCache;
|
||||
FileSystem = fileSystem;
|
||||
}
|
||||
|
||||
|
@ -44,23 +42,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
|
||||
{
|
||||
ChannelCache cache = null;
|
||||
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 = result.ToList();
|
||||
var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
|
||||
// logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
|
||||
|
||||
if (!string.IsNullOrEmpty(key) && list.Count > 0)
|
||||
{
|
||||
cache = cache ?? new ChannelCache();
|
||||
cache.Channels = list;
|
||||
_channelCache.AddOrUpdate(key, cache, (k, v) => cache);
|
||||
_memoryCache.Set(key, list);
|
||||
}
|
||||
|
||||
return list;
|
||||
|
@ -95,7 +89,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -110,7 +105,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
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);
|
||||
}
|
||||
catch (IOException)
|
||||
|
@ -233,10 +230,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
return Config.GetConfiguration<LiveTvOptions>("livetv");
|
||||
}
|
||||
|
||||
private class ChannelCache
|
||||
{
|
||||
public List<ChannelInfo> Channels;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
@ -23,7 +24,7 @@ using MediaBrowser.Model.IO;
|
|||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
@ -39,14 +40,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
public HdHomerunHost(
|
||||
IServerConfigurationManager config,
|
||||
ILogger<HdHomerunHost> logger,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IFileSystem fileSystem,
|
||||
IHttpClient httpClient,
|
||||
IServerApplicationHost appHost,
|
||||
ISocketFactory socketFactory,
|
||||
INetworkManager networkManager,
|
||||
IStreamHelper streamHelper)
|
||||
: base(config, logger, jsonSerializer, fileSystem)
|
||||
IStreamHelper streamHelper,
|
||||
IMemoryCache memoryCache)
|
||||
: base(config, logger, fileSystem, memoryCache)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_appHost = appHost;
|
||||
|
@ -75,18 +76,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
BufferContent = false
|
||||
};
|
||||
|
||||
using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
|
||||
using (var stream = response.Content)
|
||||
using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
|
||||
await using var stream = response.Content;
|
||||
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false) ?? new List<Channels>();
|
||||
|
||||
if (info.ImportFavoritesOnly)
|
||||
{
|
||||
var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
|
||||
|
||||
if (info.ImportFavoritesOnly)
|
||||
{
|
||||
lineup = lineup.Where(i => i.Favorite).ToList();
|
||||
}
|
||||
|
||||
return lineup.Where(i => !i.DRM).ToList();
|
||||
lineup = lineup.Where(i => i.Favorite).ToList();
|
||||
}
|
||||
|
||||
return lineup.Where(i => !i.DRM).ToList();
|
||||
}
|
||||
|
||||
private class HdHomerunChannelInfo : ChannelInfo
|
||||
|
@ -132,30 +132,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
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,
|
||||
BufferContent = false
|
||||
}, HttpMethod.Get).ConfigureAwait(false))
|
||||
using (var stream = response.Content)
|
||||
}, HttpMethod.Get).ConfigureAwait(false);
|
||||
await using var stream = response.Content;
|
||||
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
lock (_modelCache)
|
||||
{
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache[cacheKey] = discoverResponse;
|
||||
}
|
||||
_modelCache[cacheKey] = discoverResponse;
|
||||
}
|
||||
|
||||
return discoverResponse;
|
||||
}
|
||||
|
||||
return discoverResponse;
|
||||
}
|
||||
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 response = new DiscoverResponse
|
||||
|
@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
while (!sr.EndOfStream)
|
||||
{
|
||||
string line = StripXML(sr.ReadLine());
|
||||
if (line.Contains("Channel"))
|
||||
if (line.Contains("Channel", StringComparison.Ordinal))
|
||||
{
|
||||
LiveTvTunerStatus status;
|
||||
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
||||
|
@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
private static string StripXML(string source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
char[] buffer = new char[source.Length];
|
||||
int bufferIndex = 0;
|
||||
bool inside = false;
|
||||
|
@ -270,7 +275,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
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 isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
|
||||
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;
|
||||
|
||||
|
@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
StopStreaming(socket).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||
|
@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}
|
||||
|
||||
_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);
|
||||
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.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;
|
||||
}
|
||||
|
||||
var commandList = commands.GetCommands();
|
||||
foreach (var command in commandList)
|
||||
foreach (var command in commands.GetCommands())
|
||||
{
|
||||
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
|
||||
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);
|
||||
|
||||
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.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
|
@ -36,13 +36,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
IServerConfigurationManager config,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
ILogger<M3UTunerHost> logger,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IFileSystem fileSystem,
|
||||
IHttpClient httpClient,
|
||||
IServerApplicationHost appHost,
|
||||
INetworkManager networkManager,
|
||||
IStreamHelper streamHelper)
|
||||
: base(config, logger, jsonSerializer, fileSystem)
|
||||
IStreamHelper streamHelper,
|
||||
IMemoryCache memoryCache)
|
||||
: base(config, logger, fileSystem, memoryCache)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_appHost = appHost;
|
||||
|
|
|
@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
|
||||
{
|
||||
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 attributeValue;
|
||||
double doubleValue;
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(numberString))
|
||||
if (string.IsNullOrWhiteSpace(numberString))
|
||||
{
|
||||
// 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
|
||||
// Check for channel number with the format from SatIp
|
||||
// #EXTINF:0,84. VOX Schweiz
|
||||
// #EXTINF:0,84.0 - VOX Schweiz
|
||||
if (!string.IsNullOrWhiteSpace(nameInExtInf))
|
||||
if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace())
|
||||
{
|
||||
var numberIndex = nameInExtInf.IndexOf(' ');
|
||||
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
|
||||
{
|
||||
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
|
||||
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
|
||||
|
||||
if (!IsValidChannelNumber(numberString))
|
||||
{
|
||||
|
@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
|
||||
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -281,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
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();
|
||||
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
{
|
||||
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
|
||||
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
|
||||
"Collections": "সংকলন",
|
||||
"Collections": "কলেক্শন",
|
||||
"ChapterNameValue": "অধ্যায় {0}",
|
||||
"Channels": "চ্যানেল",
|
||||
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
|
||||
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
|
||||
"Books": "বই",
|
||||
"AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
|
||||
"Artists": "শিল্পী",
|
||||
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
|
||||
"Artists": "শিল্পীরা",
|
||||
"Application": "অ্যাপ্লিকেশন",
|
||||
"Albums": "অ্যালবামগুলো",
|
||||
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
|
||||
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
|
||||
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
|
||||
"HeaderContinueWatching": "দেখতে থাকুন",
|
||||
"HeaderCameraUploads": "ক্যামেরার আপলোডগুলো",
|
||||
"HeaderAlbumArtists": "এলবামের শিল্পী",
|
||||
"Genres": "ঘরানা",
|
||||
"HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
|
||||
"HeaderAlbumArtists": "এলবাম শিল্পী",
|
||||
"Genres": "জেনার",
|
||||
"Folders": "ফোল্ডারগুলো",
|
||||
"Favorites": "ফেভারিটগুলো",
|
||||
"FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ",
|
||||
"AppDeviceValues": "এপ: {0}, ডিভাইস: {0}",
|
||||
"Favorites": "পছন্দসমূহ",
|
||||
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
|
||||
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
|
||||
"VersionNumber": "সংস্করণ {0}",
|
||||
"ValueSpecialEpisodeName": "বিশেষ - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
|
||||
|
@ -74,20 +74,20 @@
|
|||
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
|
||||
"MusicVideos": "গানের ভিডিও",
|
||||
"Music": "গান",
|
||||
"Movies": "সিনেমা",
|
||||
"Movies": "চলচ্চিত্র",
|
||||
"MixedContent": "মিশ্র কন্টেন্ট",
|
||||
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে",
|
||||
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপ",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে",
|
||||
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে",
|
||||
"MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে",
|
||||
"Latest": "একদম নতুন",
|
||||
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
|
||||
"HeaderRecordingGroups": "রেকর্ডিং দল",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
|
||||
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
|
||||
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
|
||||
"Latest": "সর্বশেষ",
|
||||
"LabelRunningTimeValue": "চলার সময়: {0}",
|
||||
"LabelIpAddressValue": "আইপি ঠিকানা: {0}",
|
||||
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
|
||||
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
|
||||
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
|
||||
"Inherit": "থেকে পাওয়া",
|
||||
"HomeVideos": "বাসার ভিডিও",
|
||||
"HomeVideos": "হোম ভিডিও",
|
||||
"HeaderNextUp": "এরপরে আসছে",
|
||||
"HeaderLiveTV": "লাইভ টিভি",
|
||||
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
||||
"Application": "Anwendung",
|
||||
"Artists": "Interpreten",
|
||||
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
|
||||
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
|
||||
"Books": "Bücher",
|
||||
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
|
||||
"CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
|
||||
"Channels": "Kanäle",
|
||||
"ChapterNameValue": "Kapitel {0}",
|
||||
"Collections": "Sammlungen",
|
||||
|
@ -101,12 +101,12 @@
|
|||
"TaskCleanTranscode": "Lösche Transkodier Pfad",
|
||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
|
||||
"TaskUpdatePlugins": "Update Plugins",
|
||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Erneuere Schausteller",
|
||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Erneuere Schauspieler",
|
||||
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
|
||||
"TaskCleanLogs": "Lösche Log Pfad",
|
||||
"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.",
|
||||
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
|
||||
"TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
"HeaderAlbumArtists": "אמני האלבום",
|
||||
"HeaderCameraUploads": "העלאות ממצלמה",
|
||||
"HeaderContinueWatching": "המשך לצפות",
|
||||
"HeaderFavoriteAlbums": "אלבומים שאהבתי",
|
||||
"HeaderFavoriteAlbums": "אלבומים מועדפים",
|
||||
"HeaderFavoriteArtists": "אמנים מועדפים",
|
||||
"HeaderFavoriteEpisodes": "פרקים מועדפים",
|
||||
"HeaderFavoriteShows": "סדרות מועדפות",
|
||||
"HeaderFavoriteShows": "תוכניות מועדפות",
|
||||
"HeaderFavoriteSongs": "שירים מועדפים",
|
||||
"HeaderLiveTV": "שידורים חיים",
|
||||
"HeaderNextUp": "הבא",
|
||||
"HeaderNextUp": "הבא בתור",
|
||||
"HeaderRecordingGroups": "קבוצות הקלטה",
|
||||
"HomeVideos": "סרטונים בייתים",
|
||||
"Inherit": "הורש",
|
||||
|
@ -45,37 +45,37 @@
|
|||
"NameSeasonNumber": "עונה {0}",
|
||||
"NameSeasonUnknown": "עונה לא ידועה",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
|
||||
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
|
||||
"NotificationOptionAudioPlayback": "ניגון שמע החל",
|
||||
"NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
|
||||
"NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
|
||||
"NotificationOptionInstallationFailed": "התקנה נכשלה",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionPluginError": "Plugin failure",
|
||||
"NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
|
||||
"NotificationOptionPluginError": "כשלון בתוסף",
|
||||
"NotificationOptionPluginInstalled": "התוסף הותקן",
|
||||
"NotificationOptionPluginUninstalled": "התוסף הוסר",
|
||||
"NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
|
||||
"NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
|
||||
"NotificationOptionUserLockedOut": "משתמש ננעל",
|
||||
"NotificationOptionVideoPlayback": "ניגון וידאו החל",
|
||||
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
|
||||
"Photos": "תמונות",
|
||||
"Playlists": "רשימות הפעלה",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"PluginInstalledWithName": "{0} הותקן",
|
||||
"PluginUninstalledWithName": "{0} הוסר",
|
||||
"PluginUpdatedWithName": "{0} עודכן",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"ScheduledTaskStartedWithName": "{0} started",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"ScheduledTaskFailedWithName": "{0} נכשל",
|
||||
"ScheduledTaskStartedWithName": "{0} החל",
|
||||
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
|
||||
"Shows": "סדרות",
|
||||
"Songs": "שירים",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
|
||||
"Sync": "סנכרן",
|
||||
"System": "System",
|
||||
"TvShows": "סדרות טלוויזיה",
|
||||
|
@ -83,14 +83,14 @@
|
|||
"UserCreatedWithName": "המשתמש {0} נוצר",
|
||||
"UserDeletedWithName": "המשתמש {0} הוסר",
|
||||
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
|
||||
"UserLockedOutWithName": "User {0} has been locked out",
|
||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserLockedOutWithName": "המשתמש {0} ננעל",
|
||||
"UserOfflineFromDevice": "{0} התנתק מ-{1}",
|
||||
"UserOnlineFromDevice": "{0} מחובר מ-{1}",
|
||||
"UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
|
||||
"UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
|
||||
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
|
||||
"ValueSpecialEpisodeName": "מיוחד- {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskRefreshLibrary": "סרוק ספריית מדיה",
|
||||
|
@ -109,7 +109,7 @@
|
|||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
|
||||
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
|
||||
"TaskRefreshChannels": "רענן ערוץ",
|
||||
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
|
||||
"Latest": "Terbaru",
|
||||
"LabelIpAddressValue": "Alamat IP: {0}",
|
||||
"ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan",
|
||||
"ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan",
|
||||
"ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
|
||||
"ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
|
||||
"Inherit": "Warisan",
|
||||
"HomeVideos": "Video Rumah",
|
||||
"HeaderRecordingGroups": "Grup Rekaman",
|
||||
|
@ -19,8 +19,8 @@
|
|||
"HeaderFavoriteEpisodes": "Episode Favorit",
|
||||
"HeaderFavoriteArtists": "Artis Favorit",
|
||||
"HeaderFavoriteAlbums": "Album Favorit",
|
||||
"HeaderContinueWatching": "Masih Melihat",
|
||||
"HeaderCameraUploads": "Uplod Kamera",
|
||||
"HeaderContinueWatching": "Lanjutkan Menonton",
|
||||
"HeaderCameraUploads": "Unggahan Kamera",
|
||||
"HeaderAlbumArtists": "Album Artis",
|
||||
"Genres": "Genre",
|
||||
"Folders": "Folder",
|
||||
|
@ -32,11 +32,11 @@
|
|||
"ChapterNameValue": "Bagian {0}",
|
||||
"Channels": "Saluran",
|
||||
"TvShows": "Seri TV",
|
||||
"SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}",
|
||||
"StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
|
||||
"StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
|
||||
"Songs": "Lagu",
|
||||
"Playlists": "Daftar putar",
|
||||
"NotificationOptionPluginUninstalled": "Plugin dilepas",
|
||||
"NotificationOptionPluginUninstalled": "Plugin dihapus",
|
||||
"MusicVideos": "Video musik",
|
||||
"VersionNumber": "Versi {0}",
|
||||
"ValueSpecialEpisodeName": "Spesial - {0}",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"Photos": "Foto",
|
||||
"NotificationOptionUserLockedOut": "Pengguna terkunci",
|
||||
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
|
||||
"NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan",
|
||||
"NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
|
||||
"NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
|
||||
"NotificationOptionPluginInstalled": "Plugin terpasang",
|
||||
"NotificationOptionPluginError": "Kegagalan plugin",
|
||||
|
@ -74,14 +74,14 @@
|
|||
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
|
||||
"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",
|
||||
"NameSeasonNumber": "Musim {0}",
|
||||
"NameInstallFailed": "{0} instalasi gagal",
|
||||
"NameInstallFailed": "{0} penginstalan gagal",
|
||||
"Music": "Musik",
|
||||
"Movies": "Film",
|
||||
"MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui",
|
||||
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
||||
"FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
|
||||
"CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
|
||||
"DeviceOfflineWithName": "{0} telah terputus",
|
||||
|
@ -90,6 +90,28 @@
|
|||
"NotificationOptionVideoPlayback": "Pemutaran video dimulai",
|
||||
"NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
|
||||
"NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
|
||||
"MixedContent": "Konten campur",
|
||||
"PluginUninstalledWithName": "{0} telah dihapus"
|
||||
"MixedContent": "Konten campuran",
|
||||
"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",
|
||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||
"UserOfflineFromDevice": "{0} è stato disconnesso da {1}",
|
||||
"UserOnlineFromDevice": "{0} è online da {1}",
|
||||
"UserOfflineFromDevice": "{0} si è disconnesso su {1}",
|
||||
"UserOnlineFromDevice": "{0} è online su {1}",
|
||||
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
||||
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
||||
"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.",
|
||||
"TaskUpdatePlugins": "Aggiorna i Plugin",
|
||||
"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.",
|
||||
"TaskCleanLogs": "Pulisci la cartella dei log",
|
||||
"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.",
|
||||
"TaskRefreshChapterImages": "Estrai immagini capitolo",
|
||||
"TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"TvShows": "தொலைக்காட்சித் தொடர்கள்",
|
||||
"Sync": "ஒத்திசைவு",
|
||||
"StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
|
||||
"Songs": "பாட்டுகள்",
|
||||
"Songs": "பாடல்கள்",
|
||||
"Shows": "தொடர்கள்",
|
||||
"ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
|
||||
"ScheduledTaskStartedWithName": "{0} துவங்கியது",
|
||||
|
@ -93,7 +93,25 @@
|
|||
"Channels": "சேனல்கள்",
|
||||
"Books": "புத்தகங்கள்",
|
||||
"AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
|
||||
"Artists": "கலைஞர்கள்",
|
||||
"Artists": "கலைஞர்",
|
||||
"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} ஐ இயக்குகிறது"
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"MusicVideos": "Музичні відео",
|
||||
"MusicVideos": "Музичні кліпи",
|
||||
"Music": "Музика",
|
||||
"Movies": "Фільми",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server був оновлений",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server оновлено",
|
||||
"Latest": "Останні",
|
||||
"LabelIpAddressValue": "IP-адреси: {0}",
|
||||
"ItemRemovedWithName": "{0} видалено з бібліотеки",
|
||||
"ItemAddedWithName": "{0} додано до бібліотеки",
|
||||
"LabelIpAddressValue": "IP-адреса: {0}",
|
||||
"ItemRemovedWithName": "{0} видалено з медіатеки",
|
||||
"ItemAddedWithName": "{0} додано до медіатеки",
|
||||
"HeaderNextUp": "Наступний",
|
||||
"HeaderLiveTV": "Ефірне ТБ",
|
||||
"HeaderFavoriteSongs": "Улюблені пісні",
|
||||
|
@ -17,20 +17,101 @@
|
|||
"HeaderFavoriteAlbums": "Улюблені альбоми",
|
||||
"HeaderContinueWatching": "Продовжити перегляд",
|
||||
"HeaderCameraUploads": "Завантажено з камери",
|
||||
"HeaderAlbumArtists": "Виконавці альбомів",
|
||||
"HeaderAlbumArtists": "Виконавці альбому",
|
||||
"Genres": "Жанри",
|
||||
"Folders": "Директорії",
|
||||
"Folders": "Каталоги",
|
||||
"Favorites": "Улюблені",
|
||||
"DeviceOnlineWithName": "{0} під'єднано",
|
||||
"DeviceOfflineWithName": "{0} від'єднано",
|
||||
"DeviceOnlineWithName": "Пристрій {0} підключився",
|
||||
"DeviceOfflineWithName": "Пристрій {0} відключився",
|
||||
"Collections": "Колекції",
|
||||
"ChapterNameValue": "Глава {0}",
|
||||
"ChapterNameValue": "Розділ {0}",
|
||||
"Channels": "Канали",
|
||||
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
|
||||
"Books": "Книги",
|
||||
"AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
|
||||
"AuthenticationSucceededWithUserName": "{0} успішно авторизований",
|
||||
"Artists": "Виконавці",
|
||||
"Application": "Додаток",
|
||||
"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": "錄製組",
|
||||
"Inherit": "繼承",
|
||||
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
|
||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
|
||||
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
||||
"TaskRefreshChannels": "重新整理頻道",
|
||||
"TaskUpdatePlugins": "更新插件",
|
||||
|
|
|
@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization
|
|||
}
|
||||
|
||||
// Try splitting by : to handle "Germany: FSK 18"
|
||||
var index = rating.IndexOf(':');
|
||||
var index = rating.IndexOf(':', StringComparison.Ordinal);
|
||||
if (index != -1)
|
||||
{
|
||||
rating = rating.Substring(index).TrimStart(':').Trim();
|
||||
|
@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization
|
|||
throw new ArgumentNullException(nameof(culture));
|
||||
}
|
||||
|
||||
const string prefix = "Core";
|
||||
var key = prefix + culture;
|
||||
const string Prefix = "Core";
|
||||
var key = Prefix + culture;
|
||||
|
||||
return _dictionaries.GetOrAdd(
|
||||
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)
|
||||
|
|
|
@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
|
|||
public sealed class UdpSocket : ISocket, IDisposable
|
||||
{
|
||||
private Socket _socket;
|
||||
private int _localPort;
|
||||
private readonly int _localPort;
|
||||
private bool _disposed = false;
|
||||
|
||||
public Socket Socket => _socket;
|
||||
|
||||
public IPAddress LocalIPAddress { get; }
|
||||
|
||||
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
||||
{
|
||||
SocketFlags = SocketFlags.None
|
||||
|
@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
|
|||
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()
|
||||
{
|
||||
var receiveBuffer = new byte[8192];
|
||||
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
||||
_receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
|
||||
_receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
|
||||
|
||||
var sendBuffer = new byte[8192];
|
||||
_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;
|
||||
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;
|
||||
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)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
|
@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
|
|||
}
|
||||
|
||||
_socket?.Dispose();
|
||||
_receiveSocketAsyncEventArgs.Dispose();
|
||||
_sendSocketAsyncEventArgs.Dispose();
|
||||
_currentReceiveTaskCompletionSource?.TrySetCanceled();
|
||||
_currentSendTaskCompletionSource?.TrySetCanceled();
|
||||
|
||||
|
|
|
@ -152,7 +152,12 @@ namespace Emby.Server.Implementations.Networking
|
|||
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) ||
|
||||
(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] == 169 && octet[1] == 254)) // RFC3927
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
|
||||
|
@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking
|
|||
string excludeAddress = "[" + addressString + "]";
|
||||
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 [ ]
|
||||
if (Array.IndexOf(subnets, excludeAddress) != -1)
|
||||
{
|
||||
|
@ -379,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
|
|||
var host = uri.DnsSafeHost;
|
||||
_logger.LogDebug("Resolving host {0}", host);
|
||||
|
||||
address = GetIpAddresses(host).Result.FirstOrDefault();
|
||||
address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
|
||||
|
||||
if (address != null)
|
||||
{
|
||||
|
|
|
@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
|
|||
AlbumTitle = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
var hasArtist = child as IHasArtist;
|
||||
if (hasArtist != null)
|
||||
if (child is IHasArtist hasArtist)
|
||||
{
|
||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
||||
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
|
|||
AlbumTitle = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
var hasArtist = child as IHasArtist;
|
||||
if (hasArtist != null)
|
||||
if (child is IHasArtist hasArtist)
|
||||
{
|
||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
||||
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
|
|||
|
||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new M3uPlaylist();
|
||||
playlist.IsExtended = true;
|
||||
var playlist = new M3uPlaylist
|
||||
{
|
||||
IsExtended = true
|
||||
};
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
{
|
||||
var entry = new M3uPlaylistEntry()
|
||||
|
@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||
Album = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||
Album = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
|
||||
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
|
||||
{
|
||||
folderPath = folderPath + Path.DirectorySeparatorChar;
|
||||
folderPath += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
var folderUri = new Uri(folderPath);
|
||||
|
@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
|
|||
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)
|
||||
{
|
||||
var typeName = "PlaylistsFolder";
|
||||
const string TypeName = "PlaylistsFolder";
|
||||
|
||||
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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<TaskManager> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 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="jsonSerializer">The json serializer.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The filesystem manager.</param>
|
||||
public TaskManager(
|
||||
IApplicationPaths applicationPaths,
|
||||
IJsonSerializer jsonSerializer,
|
||||
ILogger<TaskManager> logger,
|
||||
IFileSystem fileSystem)
|
||||
ILogger<TaskManager> logger)
|
||||
{
|
||||
_applicationPaths = applicationPaths;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
|
|||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks
|
||||
|
@ -24,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
/// </summary>
|
||||
public class ChapterImagesTask : IScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The _logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<ChapterImagesTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
|
@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
|
||||
/// </summary>
|
||||
public ChapterImagesTask(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILibraryManager libraryManager,
|
||||
IItemRepository itemRepo,
|
||||
IApplicationPaths appPaths,
|
||||
|
@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
IFileSystem fileSystem,
|
||||
ILocalizationManager localization)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<ChapterImagesTask>();
|
||||
_libraryManager = libraryManager;
|
||||
_itemRepo = itemRepo;
|
||||
_appPaths = appPaths;
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
|||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.Serialization
|
||||
|
@ -53,10 +54,11 @@ namespace Emby.Server.Implementations.Serialization
|
|||
/// <param name="stream">The stream.</param>
|
||||
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;
|
||||
SerializeToWriter(obj, writer);
|
||||
textWriter.Formatting = Formatting.Indented;
|
||||
SerializeToWriter(obj, textWriter);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Serialization
|
|||
/// <returns>System.Object.</returns>
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
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)
|
||||
{
|
||||
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))
|
||||
|
@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
var service = httpHost.CreateInstance(serviceType);
|
||||
|
||||
var serviceRequiresContext = service as IRequiresRequest;
|
||||
if (serviceRequiresContext != null)
|
||||
if (service is IRequiresRequest serviceRequiresContext)
|
||||
{
|
||||
serviceRequiresContext.Request = req;
|
||||
}
|
||||
|
@ -189,5 +199,4 @@ namespace Emby.Server.Implementations.Services
|
|||
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
@ -105,7 +106,13 @@ namespace Emby.Server.Implementations.Services
|
|||
}
|
||||
|
||||
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
|
||||
throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName()));
|
||||
throw new NotImplementedException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Could not find method named {1}({0}) or Any({0}) on Service {2}",
|
||||
requestDto.GetType().GetMethodName(),
|
||||
expectedMethodName,
|
||||
serviceType.GetMethodName()));
|
||||
}
|
||||
|
||||
private static async Task<object> GetTaskResult(Task task)
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -44,7 +46,7 @@ namespace Emby.Server.Implementations.Services
|
|||
var pos = pathInfo.LastIndexOf('.');
|
||||
if (pos != -1)
|
||||
{
|
||||
var format = pathInfo.Substring(pos + 1);
|
||||
var format = pathInfo.AsSpan().Slice(pos + 1);
|
||||
contentType = GetFormatContentType(format);
|
||||
if (contentType != null)
|
||||
{
|
||||
|
@ -55,18 +57,21 @@ namespace Emby.Server.Implementations.Services
|
|||
return pathInfo;
|
||||
}
|
||||
|
||||
private static string GetFormatContentType(string format)
|
||||
private static string GetFormatContentType(ReadOnlySpan<char> format)
|
||||
{
|
||||
// built-in formats
|
||||
switch (format)
|
||||
if (format.Equals("json", StringComparison.Ordinal))
|
||||
{
|
||||
case "json": return "application/json";
|
||||
case "xml": return "application/xml";
|
||||
default: return null;
|
||||
return MediaTypeNames.Application.Json;
|
||||
}
|
||||
else if (format.Equals("xml", StringComparison.Ordinal))
|
||||
{
|
||||
return MediaTypeNames.Application.Xml;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
|
||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
|
||||
{
|
||||
httpReq.Items["__route"] = _restPath;
|
||||
|
||||
|
@ -75,10 +80,11 @@ namespace Emby.Server.Implementations.Services
|
|||
httpReq.ResponseContentType = _responseContentType;
|
||||
}
|
||||
|
||||
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
|
||||
var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
|
||||
|
||||
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||
|
||||
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
||||
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||
|
||||
// Apply response filters
|
||||
|
@ -90,7 +96,7 @@ namespace Emby.Server.Implementations.Services
|
|||
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
|
||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
|
||||
{
|
||||
var requestType = restPath.RequestType;
|
||||
|
||||
|
|
|
@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
var component = components[i];
|
||||
|
||||
if (component.StartsWith(VariablePrefix))
|
||||
if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
var variableName = component.Substring(1, component.Length - 2);
|
||||
if (variableName[variableName.Length - 1] == WildCardChar)
|
||||
|
@ -488,7 +488,8 @@ namespace Emby.Server.Implementations.Services
|
|||
sb.Append(value);
|
||||
for (var j = pathIx + 1; j < requestComponents.Length; j++)
|
||||
{
|
||||
sb.Append(PathSeperatorChar + requestComponents[j]);
|
||||
sb.Append(PathSeperatorChar)
|
||||
.Append(requestComponents[j]);
|
||||
}
|
||||
|
||||
value = sb.ToString();
|
||||
|
@ -505,7 +506,8 @@ namespace Emby.Server.Implementations.Services
|
|||
pathIx++;
|
||||
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
||||
sb.Append(PathSeperatorChar)
|
||||
.Append(requestComponents[pathIx++]);
|
||||
}
|
||||
|
||||
value = sb.ToString();
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
[Route("/swagger", "GET", Summary = "Gets the swagger specifications")]
|
||||
[Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")]
|
||||
public class GetSwaggerSpec : IReturn<SwaggerSpec>
|
||||
{
|
||||
}
|
||||
|
||||
public class SwaggerSpec
|
||||
{
|
||||
public string swagger { get; set; }
|
||||
|
||||
public string[] schemes { get; set; }
|
||||
|
||||
public SwaggerInfo info { get; set; }
|
||||
|
||||
public string host { get; set; }
|
||||
|
||||
public string basePath { get; set; }
|
||||
|
||||
public SwaggerTag[] tags { get; set; }
|
||||
|
||||
public IDictionary<string, Dictionary<string, SwaggerMethod>> paths { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerDefinition> definitions { get; set; }
|
||||
|
||||
public SwaggerComponents components { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerComponents
|
||||
{
|
||||
public Dictionary<string, SwaggerSecurityScheme> securitySchemes { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerSecurityScheme
|
||||
{
|
||||
public string name { get; set; }
|
||||
|
||||
public string type { get; set; }
|
||||
|
||||
public string @in { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerInfo
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
public string version { get; set; }
|
||||
|
||||
public string title { get; set; }
|
||||
|
||||
public string termsOfService { get; set; }
|
||||
|
||||
public SwaggerConcactInfo contact { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerConcactInfo
|
||||
{
|
||||
public string email { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string url { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerTag
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerMethod
|
||||
{
|
||||
public string summary { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public string[] tags { get; set; }
|
||||
|
||||
public string operationId { get; set; }
|
||||
|
||||
public string[] consumes { get; set; }
|
||||
|
||||
public string[] produces { get; set; }
|
||||
|
||||
public SwaggerParam[] parameters { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerResponse> responses { get; set; }
|
||||
|
||||
public Dictionary<string, string[]>[] security { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerParam
|
||||
{
|
||||
public string @in { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public bool required { get; set; }
|
||||
|
||||
public string type { get; set; }
|
||||
|
||||
public string collectionFormat { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerResponse
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
// ex. "$ref":"#/definitions/Pet"
|
||||
public Dictionary<string, string> schema { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerDefinition
|
||||
{
|
||||
public string type { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerProperty> properties { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerProperty
|
||||
{
|
||||
public string type { get; set; }
|
||||
|
||||
public string format { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public string[] @enum { get; set; }
|
||||
|
||||
public string @default { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerService : IService, IRequiresRequest
|
||||
{
|
||||
private readonly IHttpServer _httpServer;
|
||||
private SwaggerSpec _spec;
|
||||
|
||||
public IRequest Request { get; set; }
|
||||
|
||||
public SwaggerService(IHttpServer httpServer)
|
||||
{
|
||||
_httpServer = httpServer;
|
||||
}
|
||||
|
||||
public object Get(GetSwaggerSpec request)
|
||||
{
|
||||
return _spec ?? (_spec = GetSpec());
|
||||
}
|
||||
|
||||
private SwaggerSpec GetSpec()
|
||||
{
|
||||
string host = null;
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri))
|
||||
{
|
||||
host = uri.Host;
|
||||
}
|
||||
|
||||
var securitySchemes = new Dictionary<string, SwaggerSecurityScheme>();
|
||||
|
||||
securitySchemes["api_key"] = new SwaggerSecurityScheme
|
||||
{
|
||||
name = "api_key",
|
||||
type = "apiKey",
|
||||
@in = "query"
|
||||
};
|
||||
|
||||
var spec = new SwaggerSpec
|
||||
{
|
||||
schemes = new[] { "http" },
|
||||
tags = GetTags(),
|
||||
swagger = "2.0",
|
||||
info = new SwaggerInfo
|
||||
{
|
||||
title = "Jellyfin Server API",
|
||||
version = "1.0.0",
|
||||
description = "Explore the Jellyfin Server API",
|
||||
contact = new SwaggerConcactInfo
|
||||
{
|
||||
name = "Jellyfin Community",
|
||||
url = "https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help/"
|
||||
}
|
||||
},
|
||||
paths = GetPaths(),
|
||||
definitions = GetDefinitions(),
|
||||
basePath = "/jellyfin",
|
||||
host = host,
|
||||
|
||||
components = new SwaggerComponents
|
||||
{
|
||||
securitySchemes = securitySchemes
|
||||
}
|
||||
};
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
|
||||
private SwaggerTag[] GetTags()
|
||||
{
|
||||
return Array.Empty<SwaggerTag>();
|
||||
}
|
||||
|
||||
private Dictionary<string, SwaggerDefinition> GetDefinitions()
|
||||
{
|
||||
return new Dictionary<string, SwaggerDefinition>();
|
||||
}
|
||||
|
||||
private IDictionary<string, Dictionary<string, SwaggerMethod>> GetPaths()
|
||||
{
|
||||
var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
|
||||
|
||||
// REVIEW: this can be done better
|
||||
var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
foreach (var current in all)
|
||||
{
|
||||
foreach (var info in current.Value)
|
||||
{
|
||||
if (info.IsHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
|
||||
|| info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
paths[info.Path] = GetPathInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private Dictionary<string, SwaggerMethod> GetPathInfo(RestPath info)
|
||||
{
|
||||
var result = new Dictionary<string, SwaggerMethod>();
|
||||
|
||||
foreach (var verb in info.Verbs)
|
||||
{
|
||||
var responses = new Dictionary<string, SwaggerResponse>
|
||||
{
|
||||
{ "200", new SwaggerResponse { description = "OK" } }
|
||||
};
|
||||
|
||||
var apiKeySecurity = new Dictionary<string, string[]>
|
||||
{
|
||||
{ "api_key", Array.Empty<string>() }
|
||||
};
|
||||
|
||||
result[verb.ToLowerInvariant()] = new SwaggerMethod
|
||||
{
|
||||
summary = info.Summary,
|
||||
description = info.Description,
|
||||
produces = new[] { "application/json" },
|
||||
consumes = new[] { "application/json" },
|
||||
operationId = info.RequestType.Name,
|
||||
tags = Array.Empty<string>(),
|
||||
|
||||
parameters = Array.Empty<SwaggerParam>(),
|
||||
|
||||
responses = responses,
|
||||
|
||||
security = new[] { apiKeySecurity }
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
|
|||
/// </summary>
|
||||
/// <param name="info">The info.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="ArgumentNullException">info</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
|
||||
/// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
|
||||
public async Task OnPlaybackStopped(PlaybackStopInfo info)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
|
|
@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
|
|||
if (session != null)
|
||||
{
|
||||
EnsureController(session, e.Argument);
|
||||
await KeepAliveWebSocket(e.Argument);
|
||||
await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
|
|||
// Notify WebSocket about timeout
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket);
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
|
@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
|
|||
if (_keepAliveCancellationToken != null)
|
||||
{
|
||||
_keepAliveCancellationToken.Cancel();
|
||||
_keepAliveCancellationToken.Dispose();
|
||||
_keepAliveCancellationToken = null;
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
|
|||
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
||||
}
|
||||
|
||||
if (inactive.Any())
|
||||
if (inactive.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
||||
}
|
||||
|
@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
|
|||
{
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket);
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
|
@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
|
|||
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
if (lost.Any())
|
||||
if (lost.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
||||
foreach (var webSocket in lost)
|
||||
|
@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
|
|||
}
|
||||
}
|
||||
|
||||
if (!_webSockets.Any())
|
||||
if (_webSockets.Count == 0)
|
||||
{
|
||||
StopKeepAlive();
|
||||
}
|
||||
|
@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
|
|||
/// <returns>Task.</returns>
|
||||
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
||||
{
|
||||
return webSocket.SendAsync(new WebSocketMessage<int>
|
||||
{
|
||||
MessageType = "ForceKeepAlive",
|
||||
Data = WebSocketLostTimeout
|
||||
}, CancellationToken.None);
|
||||
return webSocket.SendAsync(
|
||||
new WebSocketMessage<int>
|
||||
{
|
||||
MessageType = "ForceKeepAlive",
|
||||
Data = WebSocketLostTimeout
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
|
|||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await callback();
|
||||
Task task = Task.Delay(interval, cancellationToken);
|
||||
await callback().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await task;
|
||||
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
|
|
@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
|
|||
|
||||
private static int CompareEpisodes(Episode x, Episode y)
|
||||
{
|
||||
var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
|
||||
var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
|
||||
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
|
||||
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
|
||||
|
||||
return xValue.CompareTo(yValue);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// All sessions will receive the message.
|
||||
/// </summary>
|
||||
AllGroup = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only the specified session will receive the message.
|
||||
/// </summary>
|
||||
CurrentSession = 1,
|
||||
|
||||
/// <summary>
|
||||
/// All sessions, except the current one, will receive the message.
|
||||
/// </summary>
|
||||
AllExceptCurrentSession = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Only sessions that are not buffering will receive the message.
|
||||
/// </summary>
|
||||
|
@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// </summary>
|
||||
private readonly GroupInfo _group = new GroupInfo();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetGroupId() => _group.GroupId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
|
||||
/// </summary>
|
||||
|
@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
_syncPlayManager = syncPlayManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetGroupId() => _group.GroupId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||
|
||||
/// <summary>
|
||||
/// Converts DateTime to UTC string.
|
||||
/// </summary>
|
||||
|
@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// <value>The UTC string.</value>
|
||||
private string DateToUTCString(DateTime date)
|
||||
{
|
||||
return date.ToUniversalTime().ToString("o");
|
||||
return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// <param name="from">The current session.</param>
|
||||
/// <param name="type">The filtering type.</param>
|
||||
/// <value>The array of sessions matching the filter.</value>
|
||||
private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
|
||||
private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BroadcastType.CurrentSession:
|
||||
return new SessionInfo[] { from };
|
||||
case BroadcastType.AllGroup:
|
||||
return _group.Participants.Values.Select(
|
||||
session => session.Session).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Select(session => session.Session);
|
||||
case BroadcastType.AllExceptCurrentSession:
|
||||
return _group.Participants.Values.Select(
|
||||
session => session.Session).Where(
|
||||
session => !session.Id.Equals(from.Id)).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Select(session => session.Session)
|
||||
.Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
|
||||
case BroadcastType.AllReady:
|
||||
return _group.Participants.Values.Where(
|
||||
session => !session.IsBuffering).Select(
|
||||
session => session.Session).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Where(session => !session.IsBuffering)
|
||||
.Select(session => session.Session);
|
||||
default:
|
||||
return Array.Empty<SessionInfo>();
|
||||
}
|
||||
|
@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
{
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
SessionInfo[] sessions = FilterSessions(from, type);
|
||||
foreach (var session in sessions)
|
||||
foreach (var session in FilterSessions(from, type))
|
||||
{
|
||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
|
||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
{
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
SessionInfo[] sessions = FilterSessions(from, type);
|
||||
foreach (var session in sessions)
|
||||
foreach (var session in FilterSessions(from, type))
|
||||
{
|
||||
yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
|
||||
yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
}
|
||||
else
|
||||
{
|
||||
var playRequest = new PlayRequest();
|
||||
playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
|
||||
playRequest.StartPositionTicks = _group.PositionTicks;
|
||||
var playRequest = new PlayRequest
|
||||
{
|
||||
ItemIds = new Guid[] { _group.PlayingItem.Id },
|
||||
StartPositionTicks = _group.PositionTicks
|
||||
};
|
||||
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
|
||||
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
|
||||
}
|
||||
|
|
35
Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
Normal file
35
Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies an action that supports the HTTP GET method.
|
||||
/// </summary>
|
||||
public class HttpSubscribeAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
public HttpSubscribeAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpSubscribeAttribute(string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
Normal file
35
Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies an action that supports the HTTP GET method.
|
||||
/// </summary>
|
||||
public class HttpUnsubscribeAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
public HttpUnsubscribeAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpUnsubscribeAttribute(string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
103
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal file
103
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal file
|
@ -0,0 +1,103 @@
|
|||
using System.Security.Claims;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Base authorization handler.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of Authorization Requirement.</typeparam>
|
||||
public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
|
||||
where T : IAuthorizationRequirement
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
protected BaseAuthorizationHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_networkManager = networkManager;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate authenticated claims.
|
||||
/// </summary>
|
||||
/// <param name="claimsPrincipal">Request claims.</param>
|
||||
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
|
||||
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
||||
/// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
|
||||
/// <returns>Validated claim status.</returns>
|
||||
protected bool ValidateClaims(
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
bool ignoreSchedule = false,
|
||||
bool localAccessOnly = false,
|
||||
bool requiredDownloadPermission = false)
|
||||
{
|
||||
// Ensure claim has userId.
|
||||
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure userId links to a valid user.
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure user is not disabled.
|
||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ip = RequestHelpers.NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
|
||||
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
|
||||
// User cannot access remotely and user is remote
|
||||
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (localAccessOnly && !isInLocalNetwork)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to access out of parental control hours.
|
||||
if (!ignoreSchedule
|
||||
&& !user.HasPermission(PermissionKind.IsAdministrator)
|
||||
&& !user.IsParentalScheduleAllowed())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to download without permission.
|
||||
if (requiredDownloadPermission
|
||||
&& !user.HasPermission(PermissionKind.EnableContentDownloading))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
|
@ -44,14 +45,24 @@ namespace Jellyfin.Api.Auth
|
|||
var authorizationInfo = _authService.Authenticate(Request);
|
||||
if (authorizationInfo == null)
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
// TODO return when legacy API is removed.
|
||||
// Don't spam the log with "Invalid User"
|
||||
// return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
|
||||
}
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
|
||||
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
|
||||
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
|
||||
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
|
||||
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
|
||||
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
|
||||
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
|
||||
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
|
||||
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Default authorization handler.
|
||||
/// </summary>
|
||||
public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public DefaultAuthorizationHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (!validated)
|
||||
{
|
||||
context.Fail();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The default authorization requirement.
|
||||
/// </summary>
|
||||
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
44
Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
Normal file
44
Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DownloadPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Download authorization handler.
|
||||
/// </summary>
|
||||
public class DownloadHandler : BaseAuthorizationHandler<DownloadRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DownloadHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public DownloadHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
11
Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
Normal file
11
Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DownloadPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The download permission requirement.
|
||||
/// </summary>
|
||||
public class DownloadRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Ignore parental control schedule and allow before startup wizard has been completed.
|
||||
/// </summary>
|
||||
public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<FirstTimeOrIgnoreParentalControlSetupRequirement>
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FirstTimeOrIgnoreParentalControlSetupHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
public FirstTimeOrIgnoreParentalControlSetupHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IConfigurationManager configurationManager)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement)
|
||||
{
|
||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// First time setup or ignore parental controls requirement.
|
||||
/// </summary>
|
||||
public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization handler for requiring first time setup or default privileges.
|
||||
/// </summary>
|
||||
public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public FirstTimeSetupOrDefaultHandler(
|
||||
IConfigurationManager configurationManager,
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement)
|
||||
{
|
||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrDefaultRequirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrDefaultRequirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
|
||||
/// </summary>
|
||||
public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,22 +1,33 @@
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization handler for requiring first time setup or elevated privileges.
|
||||
/// </summary>
|
||||
public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
||||
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configurationManager">The jellyfin configuration manager.</param>
|
||||
public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public FirstTimeSetupOrElevatedHandler(
|
||||
IConfigurationManager configurationManager,
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
|||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else if (context.User.IsInRole(UserRoles.Administrator))
|
||||
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Escape schedule controls handler.
|
||||
/// </summary>
|
||||
public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public IgnoreParentalControlHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
||||
if (!validated)
|
||||
{
|
||||
context.Fail();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user