mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-31 14:33:54 -04:00
Merge remote-tracking branch 'upstream/master' into quickconnect
This commit is contained in:
commit
a40fe86776
163
.ci/azure-pipelines-package.yml
Normal file
163
.ci/azure-pipelines-package.yml
Normal file
@ -0,0 +1,163 @@
|
||||
jobs:
|
||||
- job: BuildPackage
|
||||
displayName: 'Build Packages'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
CentOS.amd64:
|
||||
BuildConfiguration: centos.amd64
|
||||
Fedora.amd64:
|
||||
BuildConfiguration: fedora.amd64
|
||||
Debian.amd64:
|
||||
BuildConfiguration: debian.amd64
|
||||
Debian.arm64:
|
||||
BuildConfiguration: debian.arm64
|
||||
Debian.armhf:
|
||||
BuildConfiguration: debian.armhf
|
||||
Ubuntu.amd64:
|
||||
BuildConfiguration: ubuntu.amd64
|
||||
Ubuntu.arm64:
|
||||
BuildConfiguration: ubuntu.arm64
|
||||
Ubuntu.armhf:
|
||||
BuildConfiguration: ubuntu.armhf
|
||||
Linux.amd64:
|
||||
BuildConfiguration: linux.amd64
|
||||
Windows.amd64:
|
||||
BuildConfiguration: windows.amd64
|
||||
MacOS:
|
||||
BuildConfiguration: macos
|
||||
Portable:
|
||||
BuildConfiguration: portable
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
|
||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
|
||||
displayName: 'Run Dockerfile (unstable)'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
|
||||
displayName: 'Run Dockerfile (stable)'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Release'
|
||||
inputs:
|
||||
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
|
||||
artifactName: 'jellyfin-server-$(BuildConfiguration)'
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Create target directory on repository server'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'inline'
|
||||
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
||||
|
||||
- task: CopyFilesOverSSH@0
|
||||
displayName: 'Upload artifacts to repository server'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
|
||||
contents: '**'
|
||||
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
||||
|
||||
- job: BuildDocker
|
||||
displayName: 'Build Docker'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
amd64:
|
||||
BuildConfiguration: amd64
|
||||
arm64:
|
||||
BuildConfiguration: arm64
|
||||
armhf:
|
||||
BuildConfiguration: armhf
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: Docker@2
|
||||
displayName: 'Push Unstable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
repository: 'jellyfin/jellyfin-server'
|
||||
command: buildAndPush
|
||||
buildContext: '.'
|
||||
Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
|
||||
containerRegistry: Docker Hub
|
||||
tags: |
|
||||
unstable-$(Build.BuildNumber)-$(BuildConfiguration)
|
||||
unstable-$(BuildConfiguration)
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Push Stable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
inputs:
|
||||
repository: 'jellyfin/jellyfin-server'
|
||||
command: buildAndPush
|
||||
buildContext: '.'
|
||||
Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
|
||||
containerRegistry: Docker Hub
|
||||
tags: |
|
||||
stable-$(Build.BuildNumber)-$(BuildConfiguration)
|
||||
stable-$(BuildConfiguration)
|
||||
|
||||
- job: CollectArtifacts
|
||||
displayName: 'Collect Artifacts'
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
- BuildDocker
|
||||
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: SSH@0
|
||||
displayName: 'Update Unstable Repository'
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: NuGetCommand@2
|
||||
inputs:
|
||||
command: 'pack'
|
||||
packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
|
||||
packDestination: '$(Build.ArtifactStagingDirectory)'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
inputs:
|
||||
command: 'push'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||
includeNugetOrg: 'true'
|
@ -15,11 +15,13 @@ trigger:
|
||||
batch: true
|
||||
|
||||
jobs:
|
||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
||||
- template: azure-pipelines-main.yml
|
||||
parameters:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
RestoreBuildProjects: $(RestoreBuildProjects)
|
||||
|
||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
||||
- template: azure-pipelines-test.yml
|
||||
parameters:
|
||||
ImageNames:
|
||||
@ -27,6 +29,7 @@ jobs:
|
||||
Windows: 'windows-latest'
|
||||
macOS: 'macos-latest'
|
||||
|
||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
||||
- template: azure-pipelines-abi.yml
|
||||
parameters:
|
||||
Packages:
|
||||
@ -43,3 +46,6 @@ jobs:
|
||||
NugetPackageName: Jellyfin.Common
|
||||
AssemblyFileName: MediaBrowser.Common.dll
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- template: azure-pipelines-package.yml
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -39,7 +39,6 @@ ProgramData*/
|
||||
CorePlugins*/
|
||||
ProgramData-Server*/
|
||||
ProgramData-UI*/
|
||||
MediaBrowser.WebDashboard/jellyfin-web/**
|
||||
|
||||
#################
|
||||
## Visual Studio
|
||||
@ -276,4 +275,4 @@ BenchmarkDotNet.Artifacts
|
||||
# Ignore web artifacts from native builds
|
||||
web/
|
||||
web-src.*
|
||||
MediaBrowser.WebDashboard/jellyfin-web/
|
||||
MediaBrowser.WebDashboard/jellyfin-web
|
||||
|
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@ -1,7 +1,4 @@
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
@ -24,5 +21,8 @@
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
}
|
||||
,]
|
||||
],
|
||||
"env": {
|
||||
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
|
||||
}
|
||||
}
|
||||
|
7
.vscode/tasks.json
vendored
7
.vscode/tasks.json
vendored
@ -21,5 +21,10 @@
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
],
|
||||
"options": {
|
||||
"env": {
|
||||
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,12 +117,19 @@ namespace DvdLib.Ifo
|
||||
uint chapNum = 1;
|
||||
vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
|
||||
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
|
||||
if (t == null) continue;
|
||||
if (t == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
|
||||
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) break;
|
||||
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
chapNum++;
|
||||
}
|
||||
while (vtsFs.Position < (baseAddr + endaddr));
|
||||
@ -147,7 +154,10 @@ namespace DvdLib.Ifo
|
||||
uint vtsPgcOffset = vtsRead.ReadUInt32();
|
||||
|
||||
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
|
||||
if (t != null) t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
|
||||
if (t != null)
|
||||
{
|
||||
t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,14 @@ namespace DvdLib.Ifo
|
||||
Second = GetBCDValue(data[2]);
|
||||
Frames = GetBCDValue((byte)(data[3] & 0x3F));
|
||||
|
||||
if ((data[3] & 0x80) != 0) FrameRate = 30;
|
||||
else if ((data[3] & 0x40) != 0) FrameRate = 25;
|
||||
if ((data[3] & 0x80) != 0)
|
||||
{
|
||||
FrameRate = 30;
|
||||
}
|
||||
else if ((data[3] & 0x40) != 0)
|
||||
{
|
||||
FrameRate = 25;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte GetBCDValue(byte data)
|
||||
|
@ -75,8 +75,15 @@ namespace DvdLib.Ifo
|
||||
|
||||
StillTime = br.ReadByte();
|
||||
byte pbMode = br.ReadByte();
|
||||
if (pbMode == 0) PlaybackMode = ProgramPlaybackMode.Sequential;
|
||||
else PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
|
||||
if (pbMode == 0)
|
||||
{
|
||||
PlaybackMode = ProgramPlaybackMode.Sequential;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
|
||||
}
|
||||
|
||||
ProgramCount = (uint)(pbMode & 0x7F);
|
||||
|
||||
Palette = br.ReadBytes(64);
|
||||
|
@ -59,7 +59,10 @@ namespace DvdLib.Ifo
|
||||
var pgc = new ProgramChain(pgcNum);
|
||||
pgc.ParseHeader(br);
|
||||
ProgramChains.Add(pgc);
|
||||
if (entryPgc) EntryProgramChain = pgc;
|
||||
if (entryPgc)
|
||||
{
|
||||
EntryProgramChain = pgc;
|
||||
}
|
||||
|
||||
br.BaseStream.Seek(curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Emby.Dlna.Api
|
||||
{
|
||||
@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
|
||||
public string Filename { get; set; }
|
||||
}
|
||||
|
||||
public class DlnaServerService : IService, IRequiresRequest
|
||||
public class DlnaServerService : IService
|
||||
{
|
||||
private const string XMLContentType = "text/xml; charset=UTF-8";
|
||||
|
||||
@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
|
||||
public DlnaServerService(
|
||||
IDlnaManager dlnaManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
IServerConfigurationManager configurationManager)
|
||||
IServerConfigurationManager configurationManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_resultFactory = httpResultFactory;
|
||||
_configurationManager = configurationManager;
|
||||
Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||
}
|
||||
|
||||
private string GetHeader(string name)
|
||||
|
@ -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,
|
||||
|
@ -140,56 +140,74 @@ namespace Emby.Dlna
|
||||
if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
|
||||
{
|
||||
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
||||
{
|
||||
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
||||
{
|
||||
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
||||
{
|
||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
||||
{
|
||||
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
||||
{
|
||||
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
||||
{
|
||||
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
||||
{
|
||||
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
||||
{
|
||||
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -369,7 +387,7 @@ namespace Emby.Dlna
|
||||
|
||||
foreach (var name in _assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (!name.StartsWith(namespaceName))
|
||||
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -388,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -491,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>");
|
||||
|
@ -35,8 +35,6 @@ namespace Emby.Dlna.Main
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger<DlnaEntryPoint> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
private PlayToManager _manager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@ -47,14 +45,13 @@ namespace Emby.Dlna.Main
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private SsdpDevicePublisher _Publisher;
|
||||
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
private PlayToManager _manager;
|
||||
private SsdpDevicePublisher _publisher;
|
||||
private ISsdpCommunicationsServer _communicationsServer;
|
||||
|
||||
internal IContentDirectory ContentDirectory { get; private set; }
|
||||
@ -181,7 +178,7 @@ namespace Emby.Dlna.Main
|
||||
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
|
||||
OperatingSystem.Id == OperatingSystemId.Linux;
|
||||
|
||||
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
{
|
||||
IsShared = true
|
||||
};
|
||||
@ -232,20 +229,22 @@ namespace Emby.Dlna.Main
|
||||
return;
|
||||
}
|
||||
|
||||
if (_Publisher != null)
|
||||
if (_publisher != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
|
||||
_Publisher.LogFunction = LogMessage;
|
||||
_Publisher.SupportPnpRootDevice = false;
|
||||
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = LogMessage,
|
||||
SupportPnpRootDevice = false
|
||||
};
|
||||
|
||||
await RegisterServerEndpoints().ConfigureAwait(false);
|
||||
|
||||
_Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -267,6 +266,12 @@ namespace Emby.Dlna.Main
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit to LAN addresses only
|
||||
if (!_networkManager.IsAddressInSubnets(address, true, true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||
@ -288,7 +293,7 @@ namespace Emby.Dlna.Main
|
||||
};
|
||||
|
||||
SetProperies(device, fullService);
|
||||
_Publisher.AddDevice(device);
|
||||
_publisher.AddDevice(device);
|
||||
|
||||
var embeddedDevices = new[]
|
||||
{
|
||||
@ -326,7 +331,7 @@ namespace Emby.Dlna.Main
|
||||
|
||||
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
||||
{
|
||||
var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty);
|
||||
var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var serviceParts = service.Split(':');
|
||||
|
||||
@ -337,7 +342,6 @@ namespace Emby.Dlna.Main
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
|
||||
private readonly object _syncLock = new object();
|
||||
private void StartPlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
@ -416,11 +420,11 @@ namespace Emby.Dlna.Main
|
||||
|
||||
public void DisposeDevicePublisher()
|
||||
{
|
||||
if (_Publisher != null)
|
||||
if (_publisher != null)
|
||||
{
|
||||
_logger.LogInformation("Disposing SsdpDevicePublisher");
|
||||
_Publisher.Dispose();
|
||||
_Publisher = null;
|
||||
_publisher.Dispose();
|
||||
_publisher = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -19,8 +19,6 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class Device : IDisposable
|
||||
{
|
||||
#region Fields & Properties
|
||||
|
||||
private Timer _timer;
|
||||
|
||||
public DeviceInfo Properties { get; set; }
|
||||
@ -53,10 +51,10 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public Action OnDeviceUnavailable { get; set; }
|
||||
@ -142,8 +140,6 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
#region Commanding
|
||||
|
||||
public Task VolumeDown(CancellationToken cancellationToken)
|
||||
{
|
||||
var sendVolume = Math.Max(Volume - 5, 0);
|
||||
@ -212,7 +208,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
||||
if (command == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var service = GetServiceRenderingControl();
|
||||
|
||||
@ -241,7 +239,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var service = GetServiceRenderingControl();
|
||||
|
||||
@ -264,7 +264,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
@ -289,7 +291,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dictionary = new Dictionary<string, string>
|
||||
{
|
||||
@ -330,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)
|
||||
@ -402,11 +406,8 @@ namespace Emby.Dlna.PlayTo
|
||||
RestartTimer(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get data
|
||||
|
||||
private int _connectFailureCount;
|
||||
|
||||
private async void TimerCallback(object sender)
|
||||
{
|
||||
if (_disposed)
|
||||
@ -459,7 +460,9 @@ namespace Emby.Dlna.PlayTo
|
||||
_connectFailureCount = 0;
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
|
||||
if (transportState.Value == TRANSPORTSTATE.STOPPED)
|
||||
@ -479,7 +482,9 @@ namespace Emby.Dlna.PlayTo
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
|
||||
|
||||
@ -580,7 +585,9 @@ namespace Emby.Dlna.PlayTo
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result == null || result.Document == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
|
||||
.Select(i => i.Element("CurrentMute"))
|
||||
@ -870,10 +877,6 @@ namespace Emby.Dlna.PlayTo
|
||||
return new string[4];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region From XML
|
||||
|
||||
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (AvCommands != null)
|
||||
@ -1068,8 +1071,6 @@ namespace Emby.Dlna.PlayTo
|
||||
return new Device(deviceProperties, httpClient, logger, config);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
private static DeviceIcon CreateIcon(XElement element)
|
||||
{
|
||||
@ -1193,8 +1194,6 @@ namespace Emby.Dlna.PlayTo
|
||||
});
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
@ -1221,8 +1220,6 @@ namespace Emby.Dlna.PlayTo
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
|
||||
|
@ -78,9 +78,15 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var info = e.Argument;
|
||||
|
||||
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
|
||||
if (!info.Headers.TryGetValue("USN", out string usn))
|
||||
{
|
||||
usn = string.Empty;
|
||||
}
|
||||
|
||||
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
|
||||
if (!info.Headers.TryGetValue("NT", out string nt))
|
||||
{
|
||||
nt = string.Empty;
|
||||
}
|
||||
|
||||
string location = info.Location.ToString();
|
||||
|
||||
|
@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
public void AddXmlRootAttribute(string name, string value)
|
||||
{
|
||||
var atts = XmlRootAttributes ?? new XmlAttribute[] { };
|
||||
var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
|
||||
var list = atts.ToList();
|
||||
|
||||
list.Add(new XmlAttribute
|
||||
|
@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles
|
||||
},
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles
|
||||
},
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Profiles
|
||||
@ -37,7 +38,7 @@ namespace Emby.Dlna.Profiles
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Profiles
|
||||
@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Profiles
|
||||
@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Profiles
|
||||
@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Profiles
|
||||
@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
ResponseProfiles = Array.Empty<ResponseProfile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.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))
|
||||
{
|
||||
|
@ -43,9 +43,9 @@ using Emby.Server.Implementations.Security;
|
||||
using Emby.Server.Implementations.Serialization;
|
||||
using Emby.Server.Implementations.Services;
|
||||
using Emby.Server.Implementations.Session;
|
||||
using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using Emby.Server.Implementations.SyncPlay;
|
||||
using MediaBrowser.Api;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@ -79,8 +79,8 @@ using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Controller.SyncPlay;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.LocalMetadata.Savers;
|
||||
using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
@ -194,7 +194,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.
|
||||
@ -238,7 +238,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,
|
||||
@ -486,12 +486,10 @@ namespace Emby.Server.Implementations
|
||||
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
pluginBuilder.AppendLine(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1}",
|
||||
plugin.Name,
|
||||
plugin.Version));
|
||||
pluginBuilder.Append(plugin.Name)
|
||||
.Append(' ')
|
||||
.Append(plugin.Version)
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
|
||||
@ -568,10 +566,8 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
||||
// TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
|
||||
serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
|
||||
serviceCollection.AddSingleton<IMediaEncoder>(provider =>
|
||||
ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
|
||||
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||
@ -802,7 +798,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>());
|
||||
}
|
||||
@ -876,6 +871,11 @@ namespace Emby.Server.Implementations
|
||||
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
||||
continue;
|
||||
}
|
||||
catch (TypeLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Type type in exportedTypes)
|
||||
{
|
||||
@ -1158,7 +1158,7 @@ namespace Emby.Server.Implementations
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetLocalApiUrl(addresses.First());
|
||||
return GetLocalApiUrl(addresses[0]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -1231,13 +1231,13 @@ namespace Emby.Server.Implementations
|
||||
var addresses = ServerConfigurationManager
|
||||
.Configuration
|
||||
.LocalNetworkAddresses
|
||||
.Select(NormalizeConfiguredLocalAddress)
|
||||
.Select(x => NormalizeConfiguredLocalAddress(x))
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
|
||||
addresses.AddRange(_networkManager.GetLocalIpAddresses());
|
||||
}
|
||||
|
||||
var resultList = new List<IPAddress>();
|
||||
@ -1252,8 +1252,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
|
||||
if (valid)
|
||||
if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
resultList.Add(address);
|
||||
|
||||
@ -1267,13 +1266,12 @@ namespace Emby.Server.Implementations
|
||||
return resultList;
|
||||
}
|
||||
|
||||
public IPAddress NormalizeConfiguredLocalAddress(string address)
|
||||
public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
|
||||
{
|
||||
var index = address.Trim('/').IndexOf('/');
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
address = address.Substring(index + 1);
|
||||
address = address.Slice(index + 1);
|
||||
}
|
||||
|
||||
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
|
||||
|
@ -363,60 +363,4 @@ namespace Emby.Server.Implementations.Collections
|
||||
return results.Values;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The collection manager entry point.
|
||||
/// </summary>
|
||||
public sealed class CollectionManagerEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger<CollectionManagerEntryPoint> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionManagerEntryPoint"/> class.
|
||||
/// </summary>
|
||||
/// <param name="collectionManager">The collection manager.</param>
|
||||
/// <param name="config">The server configuration manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CollectionManagerEntryPoint(
|
||||
ICollectionManager collectionManager,
|
||||
IServerConfigurationManager config,
|
||||
ILogger<CollectionManagerEntryPoint> logger)
|
||||
{
|
||||
_collectionManager = (CollectionManager)collectionManager;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RunAsync()
|
||||
{
|
||||
if (!_config.Configuration.CollectionsUpgraded && _config.Configuration.IsStartupWizardCompleted)
|
||||
{
|
||||
var path = _collectionManager.GetCollectionsFolderPath();
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _collectionManager.EnsureLibraryFolder(path, true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating camera uploads library");
|
||||
}
|
||||
|
||||
_config.Configuration.CollectionsUpgraded = true;
|
||||
_config.SaveConfiguration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// Nothing to dispose
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,6 @@ namespace Emby.Server.Implementations.Configuration
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
// Validate
|
||||
if (!File.Exists(newPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
@ -133,7 +132,6 @@ namespace Emby.Server.Implementations.Configuration
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
// Validate
|
||||
if (!Directory.Exists(newPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException(
|
||||
@ -146,60 +144,5 @@ namespace Emby.Server.Implementations.Configuration
|
||||
EnsureWriteAccess(newPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all configuration values to their optimal values.
|
||||
/// </summary>
|
||||
/// <returns>If the configuration changed.</returns>
|
||||
public bool SetOptimalValues()
|
||||
{
|
||||
var config = Configuration;
|
||||
|
||||
var changed = false;
|
||||
|
||||
if (!config.EnableCaseSensitiveItemIds)
|
||||
{
|
||||
config.EnableCaseSensitiveItemIds = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.SkipDeserializationForBasicTypes)
|
||||
{
|
||||
config.SkipDeserializationForBasicTypes = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.EnableSimpleArtistDetection)
|
||||
{
|
||||
config.EnableSimpleArtistDetection = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.EnableNormalizedItemByNameIds)
|
||||
{
|
||||
config.EnableNormalizedItemByNameIds = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.DisableLiveTvChannelUserDataName)
|
||||
{
|
||||
config.DisableLiveTvChannelUserDataName = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.EnableNewOmdbSupport)
|
||||
{
|
||||
config.EnableNewOmdbSupport = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.CollectionsUpgraded)
|
||||
{
|
||||
config.CollectionsUpgraded = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
{ HostWebClientKey, bool.TrueString },
|
||||
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
|
||||
{ InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" },
|
||||
{ FfmpegProbeSizeKey, "1G" },
|
||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||
{ PlaylistsAllowDuplicatesKey, bool.TrueString }
|
||||
|
@ -1110,7 +1110,8 @@ namespace Emby.Server.Implementations.Data
|
||||
continue;
|
||||
}
|
||||
|
||||
str.Append(ToValueString(i) + "|");
|
||||
str.Append(ToValueString(i))
|
||||
.Append('|');
|
||||
}
|
||||
|
||||
str.Length -= 1; // Remove last |
|
||||
@ -2471,7 +2472,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 +2510,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,22 +2776,85 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
private string FixUnicodeChars(string buffer)
|
||||
{
|
||||
if (buffer.IndexOf('\u2013') > -1) buffer = buffer.Replace('\u2013', '-'); // en dash
|
||||
if (buffer.IndexOf('\u2014') > -1) buffer = buffer.Replace('\u2014', '-'); // em dash
|
||||
if (buffer.IndexOf('\u2015') > -1) buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
||||
if (buffer.IndexOf('\u2017') > -1) buffer = buffer.Replace('\u2017', '_'); // double low line
|
||||
if (buffer.IndexOf('\u2018') > -1) buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
||||
if (buffer.IndexOf('\u2019') > -1) buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
||||
if (buffer.IndexOf('\u201a') > -1) buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
||||
if (buffer.IndexOf('\u201b') > -1) buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
||||
if (buffer.IndexOf('\u201c') > -1) buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
||||
if (buffer.IndexOf('\u201d') > -1) buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
||||
if (buffer.IndexOf('\u201e') > -1) buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
||||
if (buffer.IndexOf('\u2026') > -1) buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
|
||||
if (buffer.IndexOf('\u2032') > -1) buffer = buffer.Replace('\u2032', '\''); // prime
|
||||
if (buffer.IndexOf('\u2033') > -1) buffer = buffer.Replace('\u2033', '\"'); // double prime
|
||||
if (buffer.IndexOf('\u0060') > -1) buffer = buffer.Replace('\u0060', '\''); // grave accent
|
||||
if (buffer.IndexOf('\u00B4') > -1) buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
||||
if (buffer.IndexOf('\u2013') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2013', '-'); // en dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2014') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2014', '-'); // em dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2015') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2017') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2017', '_'); // double low line
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2018') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2019') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201a') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201b') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201c') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201d') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201e') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2026') > -1)
|
||||
{
|
||||
buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2032') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2032', '\''); // prime
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2033') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u0060') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u00B4') > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
@ -5175,7 +5239,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));
|
||||
@ -6268,7 +6332,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;
|
||||
@ -6308,7 +6375,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
/// Gets the attachment.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>MediaAttachment</returns>
|
||||
/// <returns>MediaAttachment.</returns>
|
||||
private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader)
|
||||
{
|
||||
var item = new MediaAttachment
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IPNetwork2" Version="2.5.211" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
|
||||
<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,10 +34,10 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.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="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.1" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Udp;
|
||||
@ -47,9 +48,17 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_udpServer = new UdpServer(_logger, _appHost, _config);
|
||||
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -426,7 +426,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
|
||||
{
|
||||
bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
|
||||
|
||||
if (!noCache)
|
||||
@ -585,7 +585,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
|
||||
{
|
||||
var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest, _logger)
|
||||
var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
|
||||
{
|
||||
OnComplete = options.OnComplete
|
||||
};
|
||||
@ -622,8 +622,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <summary>
|
||||
/// Adds the caching responseHeaders.
|
||||
/// </summary>
|
||||
private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration,
|
||||
bool noCache, DateTime? lastModifiedDate)
|
||||
private void AddCachingHeaders(
|
||||
IDictionary<string, string> responseHeaders,
|
||||
TimeSpan? cacheDuration,
|
||||
bool noCache,
|
||||
DateTime? lastModifiedDate)
|
||||
{
|
||||
if (noCache)
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@ -8,52 +9,17 @@ using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the source stream.
|
||||
/// </summary>
|
||||
/// <value>The source stream.</value>
|
||||
private Stream SourceStream { get; set; }
|
||||
|
||||
private string RangeHeader { get; set; }
|
||||
|
||||
private bool IsHeadRequest { get; set; }
|
||||
|
||||
private long RangeStart { get; set; }
|
||||
|
||||
private long RangeEnd { get; set; }
|
||||
|
||||
private long RangeLength { get; set; }
|
||||
|
||||
private long TotalContentLength { get; set; }
|
||||
|
||||
public Action OnComplete { get; set; }
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private const int BufferSize = 81920;
|
||||
|
||||
/// <summary>
|
||||
/// The _options.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// The us culture.
|
||||
/// </summary>
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Additional HTTP Headers.
|
||||
/// </summary>
|
||||
/// <value>The headers.</value>
|
||||
public IDictionary<string, string> Headers => _options;
|
||||
private List<KeyValuePair<long, long?>> _requestedRanges;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
|
||||
@ -63,8 +29,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest, ILogger logger)
|
||||
public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
@ -74,7 +39,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RangeHeader = rangeHeader;
|
||||
SourceStream = source;
|
||||
IsHeadRequest = isHeadRequest;
|
||||
this._logger = logger;
|
||||
|
||||
ContentType = contentType;
|
||||
Headers[HeaderNames.ContentType] = contentType;
|
||||
@ -84,6 +48,81 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
SetRangeValues(contentLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source stream.
|
||||
/// </summary>
|
||||
/// <value>The source stream.</value>
|
||||
private Stream SourceStream { get; set; }
|
||||
private string RangeHeader { get; set; }
|
||||
private bool IsHeadRequest { get; set; }
|
||||
|
||||
private long RangeStart { get; set; }
|
||||
private long RangeEnd { get; set; }
|
||||
private long RangeLength { get; set; }
|
||||
private long TotalContentLength { get; set; }
|
||||
|
||||
public Action OnComplete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional HTTP Headers
|
||||
/// </summary>
|
||||
/// <value>The headers.</value>
|
||||
public IDictionary<string, string> Headers => _options;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the requested ranges.
|
||||
/// </summary>
|
||||
/// <value>The requested ranges.</value>
|
||||
protected List<KeyValuePair<long, long?>> RequestedRanges
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_requestedRanges == null)
|
||||
{
|
||||
_requestedRanges = new List<KeyValuePair<long, long?>>();
|
||||
|
||||
// Example: bytes=0-,32-63
|
||||
var ranges = RangeHeader.Split('=')[1].Split(',');
|
||||
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var vals = range.Split('-');
|
||||
|
||||
long start = 0;
|
||||
long? end = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(vals[0]))
|
||||
{
|
||||
start = long.Parse(vals[0], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(vals[1]))
|
||||
{
|
||||
end = long.Parse(vals[1], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
_requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
return _requestedRanges;
|
||||
}
|
||||
}
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public IRequest RequestContext { get; set; }
|
||||
|
||||
public object Response { get; set; }
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
public HttpStatusCode StatusCode
|
||||
{
|
||||
get => (HttpStatusCode)Status;
|
||||
set => Status = (int)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the range values.
|
||||
/// </summary>
|
||||
@ -115,50 +154,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _requested ranges.
|
||||
/// </summary>
|
||||
private List<KeyValuePair<long, long?>> _requestedRanges;
|
||||
/// <summary>
|
||||
/// Gets the requested ranges.
|
||||
/// </summary>
|
||||
/// <value>The requested ranges.</value>
|
||||
protected List<KeyValuePair<long, long?>> RequestedRanges
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_requestedRanges == null)
|
||||
{
|
||||
_requestedRanges = new List<KeyValuePair<long, long?>>();
|
||||
|
||||
// Example: bytes=0-,32-63
|
||||
var ranges = RangeHeader.Split('=')[1].Split(',');
|
||||
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var vals = range.Split('-');
|
||||
|
||||
long start = 0;
|
||||
long? end = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(vals[0]))
|
||||
{
|
||||
start = long.Parse(vals[0], UsCulture);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(vals[1]))
|
||||
{
|
||||
end = long.Parse(vals[1], UsCulture);
|
||||
}
|
||||
|
||||
_requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
return _requestedRanges;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@ -174,37 +169,31 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
// If the requested range is "0-", we can optimize by just doing a stream copy
|
||||
if (RangeEnd >= TotalContentLength - 1)
|
||||
{
|
||||
await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
|
||||
await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false);
|
||||
await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (OnComplete != null)
|
||||
{
|
||||
OnComplete();
|
||||
}
|
||||
OnComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength)
|
||||
private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(BufferSize);
|
||||
try
|
||||
{
|
||||
var array = new byte[BufferSize];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
|
||||
while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var bytesToCopy = Math.Min(bytesRead, copyLength);
|
||||
|
||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false);
|
||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
copyLength -= bytesToCopy;
|
||||
|
||||
@ -214,19 +203,10 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public IRequest RequestContext { get; set; }
|
||||
|
||||
public object Response { get; set; }
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
public HttpStatusCode StatusCode
|
||||
finally
|
||||
{
|
||||
get => (HttpStatusCode)Status;
|
||||
set => Status = (int)value;
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,11 +41,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
res.Headers.Add(key, value);
|
||||
}
|
||||
// Try to prevent compatibility view
|
||||
res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
|
||||
res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " +
|
||||
"Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
|
||||
"Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
|
||||
"Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
|
||||
"X-Emby-Authorization");
|
||||
"X-Emby-Authorization";
|
||||
|
||||
if (dto is Exception exception)
|
||||
{
|
||||
|
@ -13,26 +13,22 @@ 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;
|
||||
@ -51,6 +47,22 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return user;
|
||||
}
|
||||
|
||||
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||
{
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
if (auth?.User == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (auth.User.HasPermission(PermissionKind.IsDisabled))
|
||||
{
|
||||
throw new SecurityException("User account has been disabled.");
|
||||
}
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
{
|
||||
// This code is executed before the service
|
||||
|
@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.Security
|
||||
@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return GetAuthorization(requestContext);
|
||||
}
|
||||
|
||||
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(requestContext);
|
||||
var (authInfo, _) =
|
||||
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization.
|
||||
/// </summary>
|
||||
@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
private AuthorizationInfo GetAuthorization(IRequest httpReq)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
var (authInfo, originalAuthInfo) =
|
||||
GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
|
||||
|
||||
if (originalAuthInfo != null)
|
||||
{
|
||||
httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
|
||||
}
|
||||
|
||||
httpReq.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
|
||||
in Dictionary<string, string> auth,
|
||||
in IHeaderDictionary headers,
|
||||
in IQueryCollection queryString)
|
||||
{
|
||||
string deviceId = null;
|
||||
string device = null;
|
||||
string client = null;
|
||||
@ -64,20 +89,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = httpReq.Headers["X-Emby-Token"];
|
||||
token = headers["X-Emby-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = httpReq.Headers["X-MediaBrowser-Token"];
|
||||
token = headers["X-MediaBrowser-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = httpReq.QueryString["api_key"];
|
||||
token = queryString["api_key"];
|
||||
}
|
||||
|
||||
var info = new AuthorizationInfo
|
||||
var authInfo = new AuthorizationInfo
|
||||
{
|
||||
Client = client,
|
||||
Device = device,
|
||||
@ -86,6 +111,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
Token = token
|
||||
};
|
||||
|
||||
AuthenticationInfo originalAuthenticationInfo = null;
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
@ -93,81 +119,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
AccessToken = token
|
||||
});
|
||||
|
||||
var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (tokenInfo != null)
|
||||
if (originalAuthenticationInfo != null)
|
||||
{
|
||||
var updateToken = false;
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(info.Client))
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
info.Client = tokenInfo.AppName;
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(info.DeviceId))
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
{
|
||||
info.DeviceId = tokenInfo.DeviceId;
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
}
|
||||
|
||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||
var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(info.Device))
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
info.Device = tokenInfo.DeviceName;
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
tokenInfo.DeviceName = info.Device;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(info.Version))
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
info.Version = tokenInfo.AppVersion;
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
tokenInfo.AppVersion = info.Version;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
{
|
||||
tokenInfo.DateLastActivity = DateTime.UtcNow;
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (!tokenInfo.UserId.Equals(Guid.Empty))
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
info.User = _userManager.GetUserById(tokenInfo.UserId);
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
|
||||
if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tokenInfo.UserName = info.User.Username;
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(tokenInfo);
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
|
||||
}
|
||||
|
||||
httpReq.Items["AuthorizationInfo"] = info;
|
||||
|
||||
return info;
|
||||
return (authInfo, originalAuthenticationInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -187,6 +209,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return GetAuthorization(auth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the auth.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
|
||||
{
|
||||
var auth = httpReq.Headers["X-Emby-Authorization"];
|
||||
|
||||
if (string.IsNullOrEmpty(auth))
|
||||
{
|
||||
auth = httpReq.Headers[HeaderNames.Authorization];
|
||||
}
|
||||
|
||||
return GetAuthorization(auth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization.
|
||||
/// </summary>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -36,11 +36,6 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
string RestartArgs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the --plugin-manifest-url command line option.
|
||||
/// </summary>
|
||||
string PluginManifestUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the --published-server-url command line option.
|
||||
/// </summary>
|
||||
|
@ -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>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@ -13,19 +14,28 @@ namespace Emby.Server.Implementations.Library
|
||||
public class CoreResolutionIgnoreRule : IResolverIgnoreRule
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerApplicationPaths _serverApplicationPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public CoreResolutionIgnoreRule(ILibraryManager libraryManager)
|
||||
/// <param name="serverApplicationPaths">The server application paths.</param>
|
||||
public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_serverApplicationPaths = serverApplicationPaths;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
|
||||
{
|
||||
// Don't ignore application folders
|
||||
if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't ignore top level folders
|
||||
if (fileInfo.IsDirectory && parent is AggregateFolder)
|
||||
{
|
||||
@ -67,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()
|
||||
{
|
||||
|
@ -1,3 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using DotNet.Globbing;
|
||||
|
||||
@ -11,7 +14,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <summary>
|
||||
/// Files matching these glob patterns will be ignored.
|
||||
/// </summary>
|
||||
public static readonly string[] Patterns = new string[]
|
||||
private static readonly string[] _patterns =
|
||||
{
|
||||
"**/small.jpg",
|
||||
"**/albumart.jpg",
|
||||
@ -19,32 +22,51 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
// Directories
|
||||
"**/metadata/**",
|
||||
"**/metadata",
|
||||
"**/ps3_update/**",
|
||||
"**/ps3_update",
|
||||
"**/ps3_vprm/**",
|
||||
"**/ps3_vprm",
|
||||
"**/extrafanart/**",
|
||||
"**/extrafanart",
|
||||
"**/extrathumbs/**",
|
||||
"**/extrathumbs",
|
||||
"**/.actors/**",
|
||||
"**/.actors",
|
||||
"**/.wd_tv/**",
|
||||
"**/.wd_tv",
|
||||
"**/lost+found/**",
|
||||
"**/lost+found",
|
||||
|
||||
// WMC temp recording directories that will constantly be written to
|
||||
"**/TempRec/**",
|
||||
"**/TempRec",
|
||||
"**/TempSBE/**",
|
||||
"**/TempSBE",
|
||||
|
||||
// Synology
|
||||
"**/eaDir/**",
|
||||
"**/eaDir",
|
||||
"**/@eaDir/**",
|
||||
"**/@eaDir",
|
||||
"**/#recycle/**",
|
||||
"**/#recycle",
|
||||
|
||||
// Qnap
|
||||
"**/@Recycle/**",
|
||||
"**/@Recycle",
|
||||
"**/.@__thumb/**",
|
||||
"**/.@__thumb",
|
||||
"**/$RECYCLE.BIN/**",
|
||||
"**/$RECYCLE.BIN",
|
||||
"**/System Volume Information/**",
|
||||
"**/System Volume Information",
|
||||
"**/.grab/**",
|
||||
"**/.grab",
|
||||
|
||||
// Unix hidden files and directories
|
||||
"**/.*/**",
|
||||
"**/.*",
|
||||
|
||||
// thumbs.db
|
||||
"**/thumbs.db",
|
||||
@ -56,19 +78,31 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
private static readonly GlobOptions _globOptions = new GlobOptions
|
||||
{
|
||||
Evaluation = {
|
||||
Evaluation =
|
||||
{
|
||||
CaseInsensitive = true
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
|
||||
private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the supplied path should be ignored.
|
||||
/// </summary>
|
||||
public static bool ShouldIgnore(string path)
|
||||
/// <param name="path">The path to test.</param>
|
||||
/// <returns>Whether to ignore the path.</returns>
|
||||
public static bool ShouldIgnore(ReadOnlySpan<char> path)
|
||||
{
|
||||
return _globs.Any(g => g.IsMatch(path));
|
||||
int len = _globs.Length;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (_globs[i].IsMatch(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,8 @@ namespace Emby.Server.Implementations.Library
|
||||
/// </summary>
|
||||
public class LibraryManager : ILibraryManager
|
||||
{
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
|
||||
private readonly ILogger<LibraryManager> _logger;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IUserManager _userManager;
|
||||
@ -75,68 +77,29 @@ namespace Emby.Server.Implementations.Library
|
||||
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;
|
||||
|
||||
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
|
||||
|
||||
private IProviderManager ProviderManager => _providerManagerFactory.Value;
|
||||
|
||||
private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the postscan tasks.
|
||||
/// The _root folder.
|
||||
/// </summary>
|
||||
/// <value>The postscan tasks.</value>
|
||||
private ILibraryPostScanTask[] PostscanTasks { get; set; }
|
||||
private volatile AggregateFolder _rootFolder;
|
||||
private volatile UserRootFolder _userRootFolder;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the intro providers.
|
||||
/// </summary>
|
||||
/// <value>The intro providers.</value>
|
||||
private IIntroProvider[] IntroProviders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of entity resolution ignore rules.
|
||||
/// </summary>
|
||||
/// <value>The entity resolution ignore rules.</value>
|
||||
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of currently registered entity resolvers.
|
||||
/// </summary>
|
||||
/// <value>The entity resolvers enumerable.</value>
|
||||
private IItemResolver[] EntityResolvers { get; set; }
|
||||
|
||||
private IMultiItemResolver[] MultiItemResolvers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comparers.
|
||||
/// </summary>
|
||||
/// <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; }
|
||||
private bool _wizardCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="appHost">The application host</param>
|
||||
/// <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>
|
||||
@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
/// Occurs when [item added].
|
||||
/// </summary>
|
||||
/// <param name="rules">The rules.</param>
|
||||
/// <param name="resolvers">The resolvers.</param>
|
||||
/// <param name="introProviders">The intro providers.</param>
|
||||
/// <param name="itemComparers">The item comparers.</param>
|
||||
/// <param name="postscanTasks">The post scan tasks.</param>
|
||||
public void AddParts(
|
||||
IEnumerable<IResolverIgnoreRule> rules,
|
||||
IEnumerable<IItemResolver> resolvers,
|
||||
IEnumerable<IIntroProvider> introProviders,
|
||||
IEnumerable<IBaseItemComparer> itemComparers,
|
||||
IEnumerable<ILibraryPostScanTask> postscanTasks)
|
||||
{
|
||||
EntityResolutionIgnoreRules = rules.ToArray();
|
||||
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
|
||||
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
|
||||
IntroProviders = introProviders.ToArray();
|
||||
Comparers = itemComparers.ToArray();
|
||||
PostscanTasks = postscanTasks.ToArray();
|
||||
}
|
||||
public event EventHandler<ItemChangeEventArgs> ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder.
|
||||
/// Occurs when [item updated].
|
||||
/// </summary>
|
||||
private volatile AggregateFolder _rootFolder;
|
||||
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
/// Occurs when [item removed].
|
||||
/// </summary>
|
||||
private readonly object _rootFolderSyncLock = new object();
|
||||
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root folder.
|
||||
@ -241,7 +186,68 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private bool _wizardCompleted;
|
||||
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
|
||||
|
||||
private IProviderManager ProviderManager => _providerManagerFactory.Value;
|
||||
|
||||
private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the postscan tasks.
|
||||
/// </summary>
|
||||
/// <value>The postscan tasks.</value>
|
||||
private ILibraryPostScanTask[] PostscanTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the intro providers.
|
||||
/// </summary>
|
||||
/// <value>The intro providers.</value>
|
||||
private IIntroProvider[] IntroProviders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of entity resolution ignore rules.
|
||||
/// </summary>
|
||||
/// <value>The entity resolution ignore rules.</value>
|
||||
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of currently registered entity resolvers.
|
||||
/// </summary>
|
||||
/// <value>The entity resolvers enumerable.</value>
|
||||
private IItemResolver[] EntityResolvers { get; set; }
|
||||
|
||||
private IMultiItemResolver[] MultiItemResolvers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comparers.
|
||||
/// </summary>
|
||||
/// <value>The comparers.</value>
|
||||
private IBaseItemComparer[] Comparers { get; set; }
|
||||
|
||||
public bool IsScanRunning { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
/// </summary>
|
||||
/// <param name="rules">The rules.</param>
|
||||
/// <param name="resolvers">The resolvers.</param>
|
||||
/// <param name="introProviders">The intro providers.</param>
|
||||
/// <param name="itemComparers">The item comparers.</param>
|
||||
/// <param name="postscanTasks">The post scan tasks.</param>
|
||||
public void AddParts(
|
||||
IEnumerable<IResolverIgnoreRule> rules,
|
||||
IEnumerable<IItemResolver> resolvers,
|
||||
IEnumerable<IIntroProvider> introProviders,
|
||||
IEnumerable<IBaseItemComparer> itemComparers,
|
||||
IEnumerable<ILibraryPostScanTask> postscanTasks)
|
||||
{
|
||||
EntityResolutionIgnoreRules = rules.ToArray();
|
||||
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
|
||||
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
|
||||
IntroProviders = introProviders.ToArray();
|
||||
Comparers = itemComparers.ToArray();
|
||||
PostscanTasks = postscanTasks.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the configuration values.
|
||||
@ -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);
|
||||
@ -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)
|
||||
@ -514,8 +531,8 @@ namespace Emby.Server.Implementations.Library
|
||||
return key.GetMD5();
|
||||
}
|
||||
|
||||
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, bool allowIgnorePath = true)
|
||||
=> ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent, allowIgnorePath: allowIgnorePath);
|
||||
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
|
||||
=> ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
|
||||
|
||||
private BaseItem ResolvePath(
|
||||
FileSystemMetadata fileInfo,
|
||||
@ -523,8 +540,7 @@ namespace Emby.Server.Implementations.Library
|
||||
IItemResolver[] resolvers,
|
||||
Folder parent = null,
|
||||
string collectionType = null,
|
||||
LibraryOptions libraryOptions = null,
|
||||
bool allowIgnorePath = true)
|
||||
LibraryOptions libraryOptions = null)
|
||||
{
|
||||
if (fileInfo == null)
|
||||
{
|
||||
@ -548,7 +564,7 @@ namespace Emby.Server.Implementations.Library
|
||||
};
|
||||
|
||||
// Return null if ignore rules deem that we should do so
|
||||
if (allowIgnorePath && IgnoreFile(args.FileInfo, args.Parent))
|
||||
if (IgnoreFile(args.FileInfo, args.Parent))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -713,7 +729,7 @@ namespace Emby.Server.Implementations.Library
|
||||
Directory.CreateDirectory(rootFolderPath);
|
||||
|
||||
var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
|
||||
((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath), allowIgnorePath: false))
|
||||
((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
|
||||
.DeepCopy<Folder, AggregateFolder>();
|
||||
|
||||
// In case program data folder was moved
|
||||
@ -765,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)
|
||||
{
|
||||
@ -795,7 +808,7 @@ namespace Emby.Server.Implementations.Library
|
||||
if (tmpItem == null)
|
||||
{
|
||||
_logger.LogDebug("Creating new userRootFolder with DeepCopy");
|
||||
tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath), allowIgnorePath: false)).DeepCopy<Folder, UserRootFolder>();
|
||||
tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
|
||||
}
|
||||
|
||||
// In case program data folder was moved
|
||||
@ -1322,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
Items = _itemRepository.GetItemList(query).ToArray()
|
||||
Items = _itemRepository.GetItemList(query)
|
||||
};
|
||||
}
|
||||
|
||||
@ -1453,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetItems(query);
|
||||
}
|
||||
|
||||
var list = _itemRepository.GetItemList(query);
|
||||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
Items = list
|
||||
Items = _itemRepository.GetItemList(query)
|
||||
};
|
||||
}
|
||||
|
||||
@ -1793,7 +1804,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// Creates the items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="parent">The parent item</param>
|
||||
/// <param name="parent">The parent item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
|
||||
{
|
||||
@ -1866,7 +1877,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;
|
||||
@ -1894,9 +1906,19 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
|
||||
image.Width = size.Width;
|
||||
image.Height = size.Height;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path);
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@ -1925,12 +1947,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)
|
||||
{
|
||||
@ -1942,11 +1961,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)
|
||||
@ -2169,8 +2188,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,
|
||||
@ -2468,14 +2485,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
|
||||
{
|
||||
@ -2483,11 +2495,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
|
||||
var mediaInfo = _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = episode.GetMediaSources(false)[0],
|
||||
MediaType = DlnaProfileType.Video
|
||||
}, CancellationToken.None).GetAwaiter().GetResult();
|
||||
},
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (mediaInfo.ParentIndexNumber > 0)
|
||||
{
|
||||
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
|
||||
@ -2645,7 +2659,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)
|
||||
{
|
||||
@ -2662,9 +2676,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;
|
||||
}
|
||||
@ -2897,7 +2909,8 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
|
||||
if (ex.StatusCode.HasValue
|
||||
&& (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -2990,23 +3003,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);
|
||||
@ -3031,11 +3027,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);
|
||||
|
||||
@ -3074,11 +3065,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);
|
||||
|
||||
@ -3210,7 +3196,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
|
||||
mediaInfo = await _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
|
||||
}, cancellationToken).ConfigureAwait(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,11 @@ 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 readonly object _disposeLock = new object();
|
||||
|
||||
private IMediaSourceProvider[] _providers;
|
||||
|
||||
public MediaSourceManager(
|
||||
@ -368,7 +376,6 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
|
||||
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
|
||||
|
||||
@ -451,9 +458,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);
|
||||
@ -855,9 +859,6 @@ 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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
@ -869,7 +870,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
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);
|
||||
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -19,7 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
|
||||
// Only process items that are in a collection folder containing books
|
||||
if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
@ -55,7 +57,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
|
||||
// Don't return a Book if there is more (or less) than one document in the directory
|
||||
if (bookFiles.Count != 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Book
|
||||
{
|
||||
|
@ -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>
|
||||
/// <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;
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
||||
|
@ -195,13 +195,20 @@ 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);
|
||||
var name = line.Substring(0, index - 1);
|
||||
var currentChannel = line.Substring(index + 7);
|
||||
if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; }
|
||||
if (currentChannel != "none")
|
||||
{
|
||||
status = LiveTvTunerStatus.LiveTv;
|
||||
}
|
||||
else
|
||||
{
|
||||
status = LiveTvTunerStatus.Available;
|
||||
}
|
||||
|
||||
tuners.Add(new LiveTvTunerInfo
|
||||
{
|
||||
@ -219,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;
|
||||
@ -263,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;
|
||||
@ -691,7 +703,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
var model = ModelNumber ?? string.Empty;
|
||||
|
||||
if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
|
||||
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -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[] { ' ', '-' });
|
||||
|
@ -19,8 +19,8 @@
|
||||
"Sync": "Sinkroniseer",
|
||||
"HeaderFavoriteSongs": "Gunsteling Liedjies",
|
||||
"Songs": "Liedjies",
|
||||
"DeviceOnlineWithName": "{0} is verbind",
|
||||
"DeviceOfflineWithName": "{0} het afgesluit",
|
||||
"DeviceOnlineWithName": "{0} gekoppel is",
|
||||
"DeviceOfflineWithName": "{0} is ontkoppel",
|
||||
"Collections": "Versamelings",
|
||||
"Inherit": "Ontvang",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
@ -91,5 +91,9 @@
|
||||
"ChapterNameValue": "Hoofstuk",
|
||||
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
|
||||
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
|
||||
"Albums": "Albums"
|
||||
"Albums": "Albums",
|
||||
"TasksChannelsCategory": "Internet kanale",
|
||||
"TasksApplicationCategory": "aansoek",
|
||||
"TasksLibraryCategory": "biblioteek",
|
||||
"TasksMaintenanceCategory": "onderhoud"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Albums": "ألبومات",
|
||||
"Albums": "البومات",
|
||||
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
|
||||
"Application": "تطبيق",
|
||||
"Artists": "الفنانين",
|
||||
@ -14,7 +14,7 @@
|
||||
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
|
||||
"Favorites": "المفضلة",
|
||||
"Folders": "المجلدات",
|
||||
"Genres": "الأنواع",
|
||||
"Genres": "التضنيفات",
|
||||
"HeaderAlbumArtists": "فناني الألبومات",
|
||||
"HeaderCameraUploads": "تحميلات الكاميرا",
|
||||
"HeaderContinueWatching": "استئناف",
|
||||
@ -50,7 +50,7 @@
|
||||
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
|
||||
"NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي",
|
||||
"NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا",
|
||||
"NotificationOptionInstallationFailed": "فشل في التثبيت",
|
||||
"NotificationOptionInstallationFailed": "فشل التثبيت",
|
||||
"NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد",
|
||||
"NotificationOptionPluginError": "فشل في البرنامج المضاف",
|
||||
"NotificationOptionPluginInstalled": "تم تثبيت الملحق",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"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",
|
||||
"Channels": "Kanäle",
|
||||
|
@ -20,7 +20,7 @@
|
||||
"HeaderContinueWatching": "Seguir viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
"HeaderFavoriteEpisodes": "Capítulos favoritos",
|
||||
"HeaderFavoriteShows": "Programas favoritos",
|
||||
"HeaderFavoriteSongs": "Canciones favoritas",
|
||||
"HeaderLiveTV": "TV en vivo",
|
||||
|
@ -31,7 +31,7 @@
|
||||
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
||||
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||
"LabelRunningTimeValue": "Duración: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo de reproducción: {0}",
|
||||
"Latest": "Recientes",
|
||||
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"LabelRunningTimeValue": "Duración: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"Sync": "Sincronizar",
|
||||
"Songs": "Canciones",
|
||||
|
@ -5,23 +5,23 @@
|
||||
"Artists": "Izvođači",
|
||||
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
|
||||
"Books": "Knjige",
|
||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
|
||||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Poglavlje {0}",
|
||||
"Collections": "Kolekcije",
|
||||
"DeviceOfflineWithName": "{0} se odspojilo",
|
||||
"DeviceOnlineWithName": "{0} je spojeno",
|
||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
|
||||
"Favorites": "Omiljeni",
|
||||
"Favorites": "Favoriti",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Žanrovi",
|
||||
"HeaderAlbumArtists": "Izvođači albuma",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
"HeaderAlbumArtists": "Izvođači na albumu",
|
||||
"HeaderCameraUploads": "Uvoz sa kamere",
|
||||
"HeaderContinueWatching": "Nastavi gledati",
|
||||
"HeaderFavoriteAlbums": "Omiljeni albumi",
|
||||
"HeaderFavoriteArtists": "Omiljeni izvođači",
|
||||
"HeaderFavoriteEpisodes": "Omiljene epizode",
|
||||
"HeaderFavoriteShows": "Omiljene emisije",
|
||||
"HeaderFavoriteShows": "Omiljene serije",
|
||||
"HeaderFavoriteSongs": "Omiljene pjesme",
|
||||
"HeaderLiveTV": "TV uživo",
|
||||
"HeaderNextUp": "Sljedeće je",
|
||||
@ -34,23 +34,23 @@
|
||||
"LabelRunningTimeValue": "Vrijeme rada: {0}",
|
||||
"Latest": "Najnovije",
|
||||
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
|
||||
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
|
||||
"MixedContent": "Miješani sadržaj",
|
||||
"Movies": "Filmovi",
|
||||
"Music": "Glazba",
|
||||
"MusicVideos": "Glazbeni spotovi",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameInstallFailed": "{0} neuspješnih instalacija",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
||||
"NameSeasonUnknown": "Nepoznata sezona",
|
||||
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
|
||||
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
|
||||
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
|
||||
"NotificationOptionInstallationFailed": "Instalacija nije izvršena",
|
||||
"NotificationOptionInstallationFailed": "Instalacija neuspješna",
|
||||
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
|
||||
"NotificationOptionPluginError": "Dodatak otkazao",
|
||||
"NotificationOptionPluginInstalled": "Dodatak instaliran",
|
||||
@ -62,7 +62,7 @@
|
||||
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
|
||||
"Photos": "Slike",
|
||||
"Playlists": "Popisi",
|
||||
"Playlists": "Popis za reprodukciju",
|
||||
"Plugin": "Dodatak",
|
||||
"PluginInstalledWithName": "{0} je instalirano",
|
||||
"PluginUninstalledWithName": "{0} je deinstalirano",
|
||||
@ -70,15 +70,15 @@
|
||||
"ProviderValue": "Pružitelj: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} neuspjelo",
|
||||
"ScheduledTaskStartedWithName": "{0} pokrenuto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"Shows": "Shows",
|
||||
"ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
|
||||
"Shows": "Serije",
|
||||
"Songs": "Pjesme",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
|
||||
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
|
||||
"Sync": "Sink.",
|
||||
"System": "Sistem",
|
||||
"TvShows": "TV Shows",
|
||||
"TvShows": "Serije",
|
||||
"User": "Korisnik",
|
||||
"UserCreatedWithName": "Korisnik {0} je stvoren",
|
||||
"UserDeletedWithName": "Korisnik {0} je obrisan",
|
||||
@ -87,10 +87,10 @@
|
||||
"UserOfflineFromDevice": "{0} se odspojilo od {1}",
|
||||
"UserOnlineFromDevice": "{0} je online od {1}",
|
||||
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
|
||||
"ValueSpecialEpisodeName": "Specijal - {0}",
|
||||
"VersionNumber": "Verzija {0}",
|
||||
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
|
||||
@ -100,5 +100,19 @@
|
||||
"TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
|
||||
"TaskCleanCache": "Očisti priručnu memoriju",
|
||||
"TasksApplicationCategory": "Aplikacija",
|
||||
"TasksMaintenanceCategory": "Održavanje"
|
||||
"TasksMaintenanceCategory": "Održavanje",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
|
||||
"TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
|
||||
"TaskRefreshChannels": "Osvježi kanale",
|
||||
"TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
|
||||
"TaskCleanTranscode": "Očisti direktorij za transkodiranje",
|
||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
|
||||
"TaskUpdatePlugins": "Ažuriraj dodatke",
|
||||
"TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
|
||||
"TaskRefreshPeople": "Osvježi ljude",
|
||||
"TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
|
||||
"TaskCleanLogs": "Očisti direktorij sa logovima",
|
||||
"TasksChannelsCategory": "Internet kanali",
|
||||
"TasksLibraryCategory": "Biblioteka"
|
||||
}
|
||||
|
@ -57,5 +57,7 @@
|
||||
"HeaderCameraUploads": "कॅमेरा अपलोड",
|
||||
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
|
||||
"Application": "अॅप्लिकेशन",
|
||||
"AppDeviceValues": "अॅप: {0}, यंत्र: {1}"
|
||||
"AppDeviceValues": "अॅप: {0}, यंत्र: {1}",
|
||||
"Collections": "संग्रह",
|
||||
"ChapterNameValue": "धडा {0}"
|
||||
}
|
||||
|
@ -5,47 +5,47 @@
|
||||
"Artists": "Artis",
|
||||
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
|
||||
"Books": "Buku-buku",
|
||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||
"CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
|
||||
"Channels": "Saluran",
|
||||
"ChapterNameValue": "Chapter {0}",
|
||||
"ChapterNameValue": "Bab {0}",
|
||||
"Collections": "Koleksi",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOnlineWithName": "{0} is connected",
|
||||
"DeviceOfflineWithName": "{0} telah diputuskan sambungan",
|
||||
"DeviceOnlineWithName": "{0} telah disambung",
|
||||
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"Favorites": "Kegemaran",
|
||||
"Folders": "Fail-fail",
|
||||
"Genres": "Genre-genre",
|
||||
"HeaderAlbumArtists": "Album Artists",
|
||||
"HeaderAlbumArtists": "Album Artis-artis",
|
||||
"HeaderCameraUploads": "Muatnaik Kamera",
|
||||
"HeaderContinueWatching": "Terus Menonton",
|
||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||
"HeaderFavoriteArtists": "Favorite Artists",
|
||||
"HeaderFavoriteEpisodes": "Favorite Episodes",
|
||||
"HeaderFavoriteShows": "Favorite Shows",
|
||||
"HeaderFavoriteSongs": "Favorite Songs",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
"HeaderNextUp": "Next Up",
|
||||
"HeaderRecordingGroups": "Recording Groups",
|
||||
"HomeVideos": "Home videos",
|
||||
"Inherit": "Inherit",
|
||||
"ItemAddedWithName": "{0} was added to the library",
|
||||
"ItemRemovedWithName": "{0} was removed from the library",
|
||||
"HeaderFavoriteAlbums": "Album-album Kegemaran",
|
||||
"HeaderFavoriteArtists": "Artis-artis Kegemaran",
|
||||
"HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
|
||||
"HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
|
||||
"HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
|
||||
"HeaderLiveTV": "TV Siaran Langsung",
|
||||
"HeaderNextUp": "Seterusnya",
|
||||
"HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
|
||||
"HomeVideos": "Video Personal",
|
||||
"Inherit": "Mewarisi",
|
||||
"ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
|
||||
"ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
|
||||
"LabelIpAddressValue": "Alamat IP: {0}",
|
||||
"LabelRunningTimeValue": "Running time: {0}",
|
||||
"Latest": "Latest",
|
||||
"MessageApplicationUpdated": "Jellyfin Server has been updated",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
||||
"MessageServerConfigurationUpdated": "Server configuration has been updated",
|
||||
"MixedContent": "Mixed content",
|
||||
"Movies": "Movies",
|
||||
"LabelRunningTimeValue": "Masa berjalan: {0}",
|
||||
"Latest": "Terbaru",
|
||||
"MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
|
||||
"MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
|
||||
"MixedContent": "Kandungan campuran",
|
||||
"Movies": "Filem",
|
||||
"Music": "Muzik",
|
||||
"MusicVideos": "Video muzik",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameSeasonNumber": "Season {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NameInstallFailed": "{0} pemasangan gagal",
|
||||
"NameSeasonNumber": "Musim {0}",
|
||||
"NameSeasonUnknown": "Musim Tidak Diketahui",
|
||||
"NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
|
86
Emby.Server.Implementations/Localization/Core/ne.json
Normal file
86
Emby.Server.Implementations/Localization/Core/ne.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"NotificationOptionUserLockedOut": "प्रयोगकर्ता प्रतिबन्धित",
|
||||
"NotificationOptionTaskFailed": "निर्धारित कार्य विफलता",
|
||||
"NotificationOptionServerRestartRequired": "सर्भर रिस्टार्ट आवाश्यक छ",
|
||||
"NotificationOptionPluginUpdateInstalled": "प्लगइन अद्यावधिक स्थापना भयो",
|
||||
"NotificationOptionPluginUninstalled": "प्लगइन विस्थापित",
|
||||
"NotificationOptionPluginInstalled": "प्लगइन स्थापना भयो",
|
||||
"NotificationOptionPluginError": "प्लगइन असफलता",
|
||||
"NotificationOptionNewLibraryContent": "नयाँ सामग्री थपियो",
|
||||
"NotificationOptionInstallationFailed": "स्थापना असफलता",
|
||||
"NotificationOptionCameraImageUploaded": "क्यामेरा फोटो अपलोड गरियो",
|
||||
"NotificationOptionAudioPlaybackStopped": "ध्वनि प्रक्षेपण रोकियो",
|
||||
"NotificationOptionAudioPlayback": "ध्वनि प्रक्षेपण शुरू भयो",
|
||||
"NotificationOptionApplicationUpdateInstalled": "अनुप्रयोग अद्यावधिक स्थापना भयो",
|
||||
"NotificationOptionApplicationUpdateAvailable": "अनुप्रयोग अपडेट उपलब्ध छ",
|
||||
"NewVersionIsAvailable": "जेलीफिन सर्भर को नयाँ संस्करण डाउनलोड को लागी उपलब्ध छ।",
|
||||
"NameSeasonUnknown": "अज्ञात श्रृंखला",
|
||||
"NameSeasonNumber": "श्रृंखला {0}",
|
||||
"NameInstallFailed": "{0} स्थापना असफल भयो",
|
||||
"MusicVideos": "सांगीतिक भिडियोहरू",
|
||||
"Music": "संगीत",
|
||||
"Movies": "चलचित्रहरू",
|
||||
"MixedContent": "मिश्रित सामग्री",
|
||||
"MessageServerConfigurationUpdated": "सर्भर कन्फिगरेसन अद्यावधिक गरिएको छ",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "सर्भर कन्फिगरेसन विभाग {0} अद्यावधिक गरिएको छ",
|
||||
"MessageApplicationUpdatedTo": "जेलीफिन सर्भर {0} मा अद्यावधिक गरिएको छ",
|
||||
"MessageApplicationUpdated": "जेलीफिन सर्भर अपडेट गरिएको छ",
|
||||
"Latest": "नविनतम",
|
||||
"LabelRunningTimeValue": "कुल समय: {0}",
|
||||
"LabelIpAddressValue": "आईपी ठेगाना: {0}",
|
||||
"ItemRemovedWithName": "{0}लाई पुस्तकालयबाट हटाईयो",
|
||||
"ItemAddedWithName": "{0} लाईब्रेरीमा थपियो",
|
||||
"Inherit": "इनहेरिट",
|
||||
"HomeVideos": "घरेलु भिडियोहरू",
|
||||
"HeaderRecordingGroups": "रेकर्ड समूहहरू",
|
||||
"HeaderNextUp": "आगामी",
|
||||
"HeaderLiveTV": "प्रत्यक्ष टिभी",
|
||||
"HeaderFavoriteSongs": "मनपर्ने गीतहरू",
|
||||
"HeaderFavoriteShows": "मनपर्ने कार्यक्रमहरू",
|
||||
"HeaderFavoriteEpisodes": "मनपर्ने एपिसोडहरू",
|
||||
"HeaderFavoriteArtists": "मनपर्ने कलाकारहरू",
|
||||
"HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू",
|
||||
"HeaderContinueWatching": "हेर्न जारी राख्नुहोस्",
|
||||
"HeaderCameraUploads": "क्यामेरा अपलोडहरू",
|
||||
"HeaderAlbumArtists": "एल्बमका कलाकारहरू",
|
||||
"Genres": "विधाहरू",
|
||||
"Folders": "फोल्डरहरू",
|
||||
"Favorites": "मनपर्ने",
|
||||
"FailedLoginAttemptWithUserName": "{0}को लग इन प्रयास असफल",
|
||||
"DeviceOnlineWithName": "{0}को साथ जडित",
|
||||
"DeviceOfflineWithName": "{0}बाट विच्छेदन भयो",
|
||||
"Collections": "संग्रह",
|
||||
"ChapterNameValue": "अध्याय {0}",
|
||||
"Channels": "च्यानलहरू",
|
||||
"AppDeviceValues": "अनुप्रयोग: {0}, उपकरण: {1}",
|
||||
"AuthenticationSucceededWithUserName": "{0} सफलतापूर्वक प्रमाणीकरण गरियो",
|
||||
"CameraImageUploadedFrom": "{0}बाट नयाँ क्यामेरा छवि अपलोड गरिएको छ",
|
||||
"Books": "पुस्तकहरु",
|
||||
"Artists": "कलाकारहरू",
|
||||
"Application": "अनुप्रयोगहरू",
|
||||
"Albums": "एल्बमहरू",
|
||||
"TasksLibraryCategory": "पुस्तकालय",
|
||||
"TasksApplicationCategory": "अनुप्रयोग",
|
||||
"TasksMaintenanceCategory": "मर्मत",
|
||||
"UserPolicyUpdatedWithName": "प्रयोगकर्ता नीति को लागी अद्यावधिक गरिएको छ {0}",
|
||||
"UserPasswordChangedWithName": "पासवर्ड प्रयोगकर्ताका लागि परिवर्तन गरिएको छ {0}",
|
||||
"UserOnlineFromDevice": "{0} बाट अनलाइन छ {1}",
|
||||
"UserOfflineFromDevice": "{0} बाट विच्छेदन भएको छ {1}",
|
||||
"UserLockedOutWithName": "प्रयोगकर्ता {0} लक गरिएको छ",
|
||||
"UserDeletedWithName": "प्रयोगकर्ता {0} हटाइएको छ",
|
||||
"UserCreatedWithName": "प्रयोगकर्ता {0} सिर्जना गरिएको छ",
|
||||
"User": "प्रयोगकर्ता",
|
||||
"PluginInstalledWithName": "",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।",
|
||||
"Songs": "गीतहरू",
|
||||
"Shows": "शोहरू",
|
||||
"ServerNameNeedsToBeRestarted": "{0} लाई पुन: सुरु गर्नु पर्छ",
|
||||
"ScheduledTaskStartedWithName": "{0} सुरु भयो",
|
||||
"ScheduledTaskFailedWithName": "{0} असफल",
|
||||
"ProviderValue": "प्रदायक: {0}",
|
||||
"Plugin": "प्लगइनहरू",
|
||||
"Playlists": "प्लेलिस्टहरू",
|
||||
"Photos": "तस्बिरहरु",
|
||||
"NotificationOptionVideoPlaybackStopped": "भिडियो प्लेब्याक रोकियो",
|
||||
"NotificationOptionVideoPlayback": "भिडियो प्लेब्याक सुरु भयो"
|
||||
}
|
@ -101,7 +101,17 @@
|
||||
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar diretório de log",
|
||||
"TaskRefreshLibrary": "Escanear biblioteca de mídias",
|
||||
"TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.",
|
||||
"TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.",
|
||||
"TasksChannelsCategory": "Canais de Internet"
|
||||
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
|
||||
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
|
||||
"TasksChannelsCategory": "Canais de Internet",
|
||||
"TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
|
||||
"TaskDownloadMissingSubtitles": "Download das legendas em falta",
|
||||
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
|
||||
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
|
||||
"TaskCleanTranscode": "Limpar o diretório de Transcode",
|
||||
"TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
|
||||
"TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
|
||||
"TaskRefreshPeople": "Atualizar pessoas",
|
||||
"TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados."
|
||||
}
|
||||
|
@ -67,5 +67,7 @@
|
||||
"Artists": "นักแสดง",
|
||||
"Application": "แอปพลิเคชั่น",
|
||||
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
|
||||
"Albums": "อัลบั้ม"
|
||||
"Albums": "อัลบั้ม",
|
||||
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
|
||||
"ScheduledTaskFailedWithName": "{0} ล้มเหลว"
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -19,6 +19,7 @@ namespace Emby.Server.Implementations.Net
|
||||
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
|
||||
try
|
||||
{
|
||||
retVal.EnableBroadcast = true;
|
||||
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
|
||||
|
||||
@ -46,6 +47,7 @@ namespace Emby.Server.Implementations.Net
|
||||
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
|
||||
try
|
||||
{
|
||||
retVal.EnableBroadcast = true;
|
||||
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
|
||||
|
||||
@ -112,6 +114,7 @@ namespace Emby.Server.Implementations.Net
|
||||
|
||||
try
|
||||
{
|
||||
retVal.EnableBroadcast = true;
|
||||
// retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
|
||||
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
|
||||
|
||||
|
@ -37,7 +37,10 @@ namespace Emby.Server.Implementations.Net
|
||||
|
||||
public UdpSocket(Socket socket, int localPort, IPAddress ip)
|
||||
{
|
||||
if (socket == null) throw new ArgumentNullException(nameof(socket));
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_localPort = localPort;
|
||||
@ -103,7 +106,10 @@ namespace Emby.Server.Implementations.Net
|
||||
|
||||
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
||||
{
|
||||
if (socket == null) throw new ArgumentNullException(nameof(socket));
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_socket.Connect(endPoint);
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to take care of network interface management.
|
||||
/// </summary>
|
||||
public class NetworkManager : INetworkManager
|
||||
{
|
||||
private readonly ILogger<NetworkManager> _logger;
|
||||
@ -21,8 +23,14 @@ namespace Emby.Server.Implementations.Networking
|
||||
private readonly object _localIpAddressSyncLock = new object();
|
||||
|
||||
private readonly object _subnetLookupLock = new object();
|
||||
private Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
||||
|
||||
private List<PhysicalAddress> _macAddresses;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NetworkManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to use for messages.</param>
|
||||
public NetworkManager(ILogger<NetworkManager> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
@ -31,8 +39,10 @@ namespace Emby.Server.Implementations.Networking
|
||||
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler NetworkChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Func<string[]> LocalSubnetsFn { get; set; }
|
||||
|
||||
private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
|
||||
@ -58,13 +68,14 @@ namespace Emby.Server.Implementations.Networking
|
||||
NetworkChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface = true)
|
||||
/// <inheritdoc/>
|
||||
public IPAddress[] GetLocalIpAddresses()
|
||||
{
|
||||
lock (_localIpAddressSyncLock)
|
||||
{
|
||||
if (_localIpAddresses == null)
|
||||
{
|
||||
var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).ToArray();
|
||||
var addresses = GetLocalIpAddressesInternal().ToArray();
|
||||
|
||||
_localIpAddresses = addresses;
|
||||
}
|
||||
@ -73,42 +84,47 @@ namespace Emby.Server.Implementations.Networking
|
||||
}
|
||||
}
|
||||
|
||||
private List<IPAddress> GetLocalIpAddressesInternal(bool ignoreVirtualInterface)
|
||||
private List<IPAddress> GetLocalIpAddressesInternal()
|
||||
{
|
||||
var list = GetIPsDefault(ignoreVirtualInterface).ToList();
|
||||
var list = GetIPsDefault().ToList();
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList();
|
||||
}
|
||||
|
||||
var listClone = list.ToList();
|
||||
var listClone = new List<IPAddress>();
|
||||
|
||||
return list
|
||||
var subnets = LocalSubnetsFn();
|
||||
|
||||
foreach (var i in list)
|
||||
{
|
||||
if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.IndexOf(subnets, $"[{i}]") == -1)
|
||||
{
|
||||
listClone.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return listClone
|
||||
.OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1)
|
||||
.ThenBy(i => listClone.IndexOf(i))
|
||||
.Where(FilterIpAddress)
|
||||
// .ThenBy(i => listClone.IndexOf(i))
|
||||
.GroupBy(i => i.ToString())
|
||||
.Select(x => x.First())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool FilterIpAddress(IPAddress address)
|
||||
{
|
||||
if (address.IsIPv6LinkLocal
|
||||
|| address.ToString().StartsWith("169.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInPrivateAddressSpace(string endpoint)
|
||||
{
|
||||
return IsInPrivateAddressSpace(endpoint, true);
|
||||
}
|
||||
|
||||
// Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address
|
||||
private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets)
|
||||
{
|
||||
if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase))
|
||||
@ -116,12 +132,12 @@ namespace Emby.Server.Implementations.Networking
|
||||
return true;
|
||||
}
|
||||
|
||||
// ipv6
|
||||
// IPV6
|
||||
if (endpoint.Split('.').Length > 4)
|
||||
{
|
||||
// Handle ipv4 mapped to ipv6
|
||||
var originalEndpoint = endpoint;
|
||||
endpoint = endpoint.Replace("::ffff:", string.Empty);
|
||||
endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -130,23 +146,26 @@ namespace Emby.Server.Implementations.Networking
|
||||
}
|
||||
|
||||
// Private address space:
|
||||
// http://en.wikipedia.org/wiki/Private_network
|
||||
|
||||
if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Is172AddressPrivate(endpoint);
|
||||
}
|
||||
|
||||
if (endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||
endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
|
||||
endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkSubnets && endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase))
|
||||
if (!IPAddress.TryParse(endpoint, out var ipAddress))
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] octet = ipAddress.GetAddressBytes();
|
||||
|
||||
if ((octet[0] == 10) ||
|
||||
(octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
|
||||
(octet[0] == 192 && octet[1] == 168) || // RFC1918
|
||||
(octet[0] == 127) || // RFC1122
|
||||
(octet[0] == 169 && octet[1] == 254)) // RFC3927
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
|
||||
@ -157,6 +176,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint)
|
||||
{
|
||||
if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase))
|
||||
@ -179,6 +199,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart
|
||||
private List<string> GetSubnets(string endpointFirstPart)
|
||||
{
|
||||
lock (_subnetLookupLock)
|
||||
@ -224,41 +245,71 @@ namespace Emby.Server.Implementations.Networking
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Is172AddressPrivate(string endpoint)
|
||||
{
|
||||
for (var i = 16; i <= 31; i++)
|
||||
{
|
||||
if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(string endpoint)
|
||||
{
|
||||
return IsInLocalNetworkInternal(endpoint, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAddressInSubnets(string addressString, string[] subnets)
|
||||
{
|
||||
return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
|
||||
{
|
||||
byte[] octet = address.GetAddressBytes();
|
||||
|
||||
if ((octet[0] == 127) || // RFC1122
|
||||
(octet[0] == 169 && octet[1] == 254)) // RFC3927
|
||||
{
|
||||
// don't use on loopback or 169 interfaces
|
||||
return false;
|
||||
}
|
||||
|
||||
string addressString = address.ToString();
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsAddressInSubnets(address, addressString, subnets);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format.
|
||||
/// </summary>
|
||||
/// <param name="address">IPAddress version of the address.</param>
|
||||
/// <param name="addressString">The address to check.</param>
|
||||
/// <param name="subnets">If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address.</param>
|
||||
/// <returns><c>false</c>if the address isn't in the subnets, <c>true</c> otherwise.</returns>
|
||||
private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets)
|
||||
{
|
||||
foreach (var subnet in subnets)
|
||||
{
|
||||
var normalizedSubnet = subnet.Trim();
|
||||
|
||||
// Is the subnet a host address and does it match the address being passes?
|
||||
if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse CIDR subnets and see if address falls within it.
|
||||
if (normalizedSubnet.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
try
|
||||
{
|
||||
var ipNetwork = IPNetwork.Parse(normalizedSubnet);
|
||||
if (ipNetwork.Contains(address))
|
||||
@ -266,6 +317,11 @@ namespace Emby.Server.Implementations.Networking
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignoring - invalid subnet passed encountered.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -288,7 +344,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
var localSubnets = localSubnetsFn();
|
||||
foreach (var subnet in localSubnets)
|
||||
{
|
||||
// only validate if there's at least one valid entry
|
||||
// Only validate if there's at least one valid entry.
|
||||
if (!string.IsNullOrWhiteSpace(subnet))
|
||||
{
|
||||
return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false);
|
||||
@ -334,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)
|
||||
{
|
||||
@ -345,7 +401,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Can happen with reverse proxy or IIS url rewriting
|
||||
// Can happen with reverse proxy or IIS url rewriting?
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -362,7 +418,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
return Dns.GetHostAddressesAsync(hostName);
|
||||
}
|
||||
|
||||
private IEnumerable<IPAddress> GetIPsDefault(bool ignoreVirtualInterface)
|
||||
private IEnumerable<IPAddress> GetIPsDefault()
|
||||
{
|
||||
IEnumerable<NetworkInterface> interfaces;
|
||||
|
||||
@ -382,15 +438,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
{
|
||||
var ipProperties = network.GetIPProperties();
|
||||
|
||||
// Try to exclude virtual adapters
|
||||
// http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
|
||||
var addr = ipProperties.GatewayAddresses.FirstOrDefault();
|
||||
if (addr == null
|
||||
|| (ignoreVirtualInterface
|
||||
&& (addr.Address.Equals(IPAddress.Any) || addr.Address.Equals(IPAddress.IPv6Any))))
|
||||
{
|
||||
return Enumerable.Empty<IPAddress>();
|
||||
}
|
||||
// Exclude any addresses if they appear in the LAN list in [ ]
|
||||
|
||||
return ipProperties.UnicastAddresses
|
||||
.Select(i => i.Address)
|
||||
@ -423,33 +471,29 @@ namespace Emby.Server.Implementations.Networking
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetRandomUnusedUdpPort()
|
||||
{
|
||||
var localEndPoint = new IPEndPoint(IPAddress.Any, 0);
|
||||
using (var udpClient = new UdpClient(localEndPoint))
|
||||
{
|
||||
var port = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
return port;
|
||||
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
}
|
||||
}
|
||||
|
||||
private List<PhysicalAddress> _macAddresses;
|
||||
/// <inheritdoc/>
|
||||
public List<PhysicalAddress> GetMacAddresses()
|
||||
{
|
||||
if (_macAddresses == null)
|
||||
{
|
||||
_macAddresses = GetMacAddressesInternal().ToList();
|
||||
}
|
||||
|
||||
return _macAddresses;
|
||||
return _macAddresses ??= GetMacAddressesInternal().ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<PhysicalAddress> GetMacAddressesInternal()
|
||||
=> NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
|
||||
.Select(x => x.GetPhysicalAddress())
|
||||
.Where(x => x != null && x != PhysicalAddress.None);
|
||||
.Where(x => !x.Equals(PhysicalAddress.None));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask)
|
||||
{
|
||||
IPAddress network1 = GetNetworkAddress(address1, subnetMask);
|
||||
@ -476,6 +520,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
return new IPAddress(broadcastAddress);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPAddress GetLocalIpSubnetMask(IPAddress address)
|
||||
{
|
||||
NetworkInterface[] interfaces;
|
||||
@ -495,8 +540,6 @@ namespace Emby.Server.Implementations.Networking
|
||||
}
|
||||
|
||||
foreach (NetworkInterface ni in interfaces)
|
||||
{
|
||||
if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null)
|
||||
{
|
||||
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
|
||||
{
|
||||
@ -506,7 +549,6 @@ namespace Emby.Server.Implementations.Networking
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -539,13 +539,21 @@ namespace Emby.Server.Implementations.Playlists
|
||||
|
||||
private static string UnEscape(string content)
|
||||
{
|
||||
if (content == null) return content;
|
||||
if (content == null)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
return content.Replace("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<");
|
||||
}
|
||||
|
||||
private static string Escape(string content)
|
||||
{
|
||||
if (content == null) return null;
|
||||
if (content == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<");
|
||||
}
|
||||
|
||||
|
@ -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>();
|
||||
}
|
||||
@ -95,7 +90,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||
/// Queues the scheduled task.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="options">Task options</param>
|
||||
/// <param name="options">Task options.</param>
|
||||
public void QueueScheduledTask<T>(TaskOptions options)
|
||||
where T : IScheduledTask
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Security
|
||||
statement.TryBind("@AppName", info.AppName);
|
||||
statement.TryBind("@AppVersion", info.AppVersion);
|
||||
statement.TryBind("@DeviceName", info.DeviceName);
|
||||
statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture)));
|
||||
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
|
||||
statement.TryBind("@UserName", info.UserName);
|
||||
statement.TryBind("@IsActive", true);
|
||||
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
|
||||
@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.Security
|
||||
statement.TryBind("@AppName", info.AppName);
|
||||
statement.TryBind("@AppVersion", info.AppVersion);
|
||||
statement.TryBind("@DeviceName", info.DeviceName);
|
||||
statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture)));
|
||||
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
|
||||
statement.TryBind("@UserName", info.UserName);
|
||||
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
|
||||
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
|
||||
|
@ -40,7 +40,9 @@ namespace Emby.Server.Implementations.Services
|
||||
if (httpResult != null)
|
||||
{
|
||||
if (httpResult.RequestContext == null)
|
||||
{
|
||||
httpResult.RequestContext = request;
|
||||
}
|
||||
|
||||
response.StatusCode = httpResult.Status;
|
||||
}
|
||||
|
@ -144,7 +144,10 @@ namespace Emby.Server.Implementations.Services
|
||||
var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
|
||||
foreach (var potentialHashMatch in yieldedWildcardMatches)
|
||||
{
|
||||
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue;
|
||||
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bestScore = -1;
|
||||
RestPath bestMatch = null;
|
||||
@ -186,5 +189,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;
|
||||
@ -42,11 +43,15 @@ namespace Emby.Server.Implementations.Services
|
||||
}
|
||||
|
||||
if (mi.GetParameters().Length != 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var actionName = mi.Name;
|
||||
if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(mi);
|
||||
}
|
||||
@ -63,7 +68,10 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
foreach (var actionCtx in actions)
|
||||
{
|
||||
if (execMap.ContainsKey(actionCtx.Id)) continue;
|
||||
if (execMap.ContainsKey(actionCtx.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
execMap[actionCtx.Id] = actionCtx;
|
||||
}
|
||||
@ -98,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,15 +57,18 @@ 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)
|
||||
@ -79,6 +84,7 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||
|
||||
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
||||
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||
|
||||
// Apply response filters
|
||||
|
@ -124,7 +124,10 @@ namespace Emby.Server.Implementations.Services
|
||||
var hasSeparators = new List<bool>();
|
||||
foreach (var component in this.restPath.Split(PathSeperatorChar))
|
||||
{
|
||||
if (string.IsNullOrEmpty(component)) continue;
|
||||
if (string.IsNullOrEmpty(component))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& component.IndexOf(ComponentSeperator) != -1)
|
||||
@ -153,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)
|
||||
@ -302,9 +305,9 @@ namespace Emby.Server.Implementations.Services
|
||||
}
|
||||
|
||||
// Routes with least wildcard matches get the highest score
|
||||
var score = Math.Max((100 - wildcardMatchCount), 1) * 1000
|
||||
var score = Math.Max(100 - wildcardMatchCount, 1) * 1000
|
||||
// Routes with less variable (and more literal) matches
|
||||
+ Math.Max((10 - VariableArgsCount), 1) * 100;
|
||||
+ Math.Max(10 - VariableArgsCount, 1) * 100;
|
||||
|
||||
// Exact verb match is better than ANY
|
||||
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
|
||||
@ -442,6 +445,7 @@ namespace Emby.Server.Implementations.Services
|
||||
&& requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
|
||||
|
||||
if (!isValidWildCardPath)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@ -449,6 +453,7 @@ namespace Emby.Server.Implementations.Services
|
||||
pathInfo,
|
||||
this.restPath));
|
||||
}
|
||||
}
|
||||
|
||||
var requestKeyValuesMap = new Dictionary<string, string>();
|
||||
var pathIx = 0;
|
||||
@ -483,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();
|
||||
@ -500,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();
|
||||
|
@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
catch (DbUpdateConcurrencyException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Error updating user's last activity date.");
|
||||
_logger.LogDebug(e, "Error updating user's last activity date.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -502,7 +502,8 @@ namespace Emby.Server.Implementations.Session
|
||||
Client = appName,
|
||||
DeviceId = deviceId,
|
||||
ApplicationVersion = appVersion,
|
||||
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
|
||||
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
||||
ServerId = _appHost.SystemId
|
||||
};
|
||||
|
||||
var username = user?.Username;
|
||||
|
@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
|
||||
public int Compare(BaseItem x, BaseItem y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(x));
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
return DateTime.Compare(x.DateCreated, y.DateCreated);
|
||||
}
|
||||
|
@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
|
||||
public int Compare(BaseItem x, BaseItem y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(x));
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
|
||||
}
|
||||
|
@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
|
||||
public int Compare(BaseItem x, BaseItem y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(x));
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
@ -194,26 +194,24 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
|
||||
public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
|
||||
{
|
||||
_group.AddSession(session);
|
||||
_syncPlayManager.AddSessionToGroup(session, this);
|
||||
|
||||
_group.PlayingItem = session.FullNowPlayingItem;
|
||||
_group.IsPaused = true;
|
||||
_group.IsPaused = session.PlayState.IsPaused;
|
||||
_group.PositionTicks = session.PlayState.PositionTicks ?? 0;
|
||||
_group.LastActivity = DateTime.UtcNow;
|
||||
|
||||
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
|
||||
SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
|
||||
var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
|
||||
SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
|
||||
if (session.NowPlayingItem?.Id == _group.PlayingItem.Id)
|
||||
{
|
||||
_group.AddSession(session);
|
||||
_syncPlayManager.AddSessionToGroup(session, this);
|
||||
@ -224,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
|
||||
SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
|
||||
|
||||
// Client join and play, syncing will happen client side
|
||||
// Syncing will happen client-side
|
||||
if (!_group.IsPaused)
|
||||
{
|
||||
var playCommand = NewSyncPlayCommand(SendCommandType.Play);
|
||||
@ -262,10 +260,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
/// <inheritdoc />
|
||||
public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// The server's job is to mantain a consistent state to which clients refer to,
|
||||
// as also to notify clients of state changes.
|
||||
// The actual syncing of media playback happens client side.
|
||||
// Clients are aware of the server's time and use it to sync.
|
||||
// The server's job is to maintain a consistent state for clients to reference
|
||||
// and notify clients of state changes. The actual syncing of media playback
|
||||
// happens client side. Clients are aware of the server's time and use it to sync.
|
||||
switch (request.Type)
|
||||
{
|
||||
case PlaybackRequestType.Play:
|
||||
@ -277,13 +274,13 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
case PlaybackRequestType.Seek:
|
||||
HandleSeekRequest(session, request, cancellationToken);
|
||||
break;
|
||||
case PlaybackRequestType.Buffering:
|
||||
case PlaybackRequestType.Buffer:
|
||||
HandleBufferingRequest(session, request, cancellationToken);
|
||||
break;
|
||||
case PlaybackRequestType.BufferingDone:
|
||||
case PlaybackRequestType.Ready:
|
||||
HandleBufferingDoneRequest(session, request, cancellationToken);
|
||||
break;
|
||||
case PlaybackRequestType.UpdatePing:
|
||||
case PlaybackRequestType.Ping:
|
||||
HandlePingUpdateRequest(session, request);
|
||||
break;
|
||||
}
|
||||
@ -301,7 +298,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
{
|
||||
// Pick a suitable time that accounts for latency
|
||||
var delay = _group.GetHighestPing() * 2;
|
||||
delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
|
||||
delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
|
||||
|
||||
// Unpause group and set starting point in future
|
||||
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
|
||||
@ -337,8 +334,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
var currentTime = DateTime.UtcNow;
|
||||
var elapsedTime = currentTime - _group.LastActivity;
|
||||
_group.LastActivity = currentTime;
|
||||
|
||||
// Seek only if playback actually started
|
||||
// (a pause request may be issued during the delay added to account for latency)
|
||||
// Pause request may be issued during the delay added to account for latency
|
||||
_group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
|
||||
|
||||
var command = NewSyncPlayCommand(SendCommandType.Pause);
|
||||
@ -451,7 +449,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
{
|
||||
// Client, that was buffering, resumed playback but did not update others in time
|
||||
delay = _group.GetHighestPing() * 2;
|
||||
delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
|
||||
delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
|
||||
|
||||
_group.LastActivity = currentTime.AddMilliseconds(
|
||||
delay);
|
||||
@ -495,7 +493,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
|
||||
{
|
||||
// Collected pings are used to account for network latency when unpausing playback
|
||||
_group.UpdatePing(session, request.Ping ?? _group.DefaulPing);
|
||||
_group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -170,10 +170,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
{
|
||||
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
|
||||
|
||||
var error = new GroupUpdate<string>()
|
||||
var error = new GroupUpdate<string>
|
||||
{
|
||||
Type = GroupUpdateType.CreateGroupDenied
|
||||
};
|
||||
|
||||
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
var group = new SyncPlayController(_sessionManager, this);
|
||||
_groups[group.GetGroupId()] = group;
|
||||
|
||||
group.InitGroup(session, cancellationToken);
|
||||
group.CreateGroup(session, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,6 +206,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
{
|
||||
Type = GroupUpdateType.JoinGroupDenied
|
||||
};
|
||||
|
||||
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
@ -300,9 +302,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
|
||||
group => group.GetInfo()).ToList();
|
||||
}
|
||||
// Otherwise show all available groups
|
||||
else
|
||||
{
|
||||
// Otherwise show all available groups
|
||||
return _groups.Values.Where(
|
||||
group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
|
||||
group => group.GetInfo()).ToList();
|
||||
@ -322,6 +324,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
{
|
||||
Type = GroupUpdateType.JoinGroupDenied
|
||||
};
|
||||
|
||||
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
@ -366,7 +369,6 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
}
|
||||
|
||||
_sessionToGroupMap.Remove(session.Id, out var tempGroup);
|
||||
|
||||
if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
|
||||
{
|
||||
throw new InvalidOperationException("Session was in wrong group!");
|
||||
|
@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
|
||||
limit = limit.Value + 10;
|
||||
}
|
||||
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
var items = _libraryManager
|
||||
.GetItemList(
|
||||
new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Episode).Name },
|
||||
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
|
||||
SeriesPresentationUniqueKey = presentationUniqueKey,
|
||||
Limit = limit,
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = new ItemFields[]
|
||||
{
|
||||
ItemFields.SeriesPresentationUniqueKey
|
||||
},
|
||||
EnableImages = false
|
||||
},
|
||||
DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
|
||||
GroupBySeriesPresentationUniqueKey = true
|
||||
|
||||
}, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey);
|
||||
}, parentsFolders.ToList())
|
||||
.Cast<Episode>()
|
||||
.Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
|
||||
.Select(GetUniqueSeriesKey);
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
|
||||
|
@ -1,7 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -17,11 +16,10 @@ using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Updates
|
||||
@ -31,11 +29,6 @@ namespace Emby.Server.Implementations.Updates
|
||||
/// </summary>
|
||||
public class InstallationManager : IInstallationManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for a setting that specifies a URL for the plugin repository JSON manifest.
|
||||
/// </summary>
|
||||
public const string PluginManifestUrlKey = "InstallationManager:PluginManifestUrl";
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
@ -53,7 +46,6 @@ namespace Emby.Server.Implementations.Updates
|
||||
private readonly IApplicationHost _applicationHost;
|
||||
|
||||
private readonly IZipClient _zipClient;
|
||||
private readonly IConfiguration _appConfig;
|
||||
|
||||
private readonly object _currentInstallationsLock = new object();
|
||||
|
||||
@ -75,8 +67,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
IJsonSerializer jsonSerializer,
|
||||
IServerConfigurationManager config,
|
||||
IFileSystem fileSystem,
|
||||
IZipClient zipClient,
|
||||
IConfiguration appConfig)
|
||||
IZipClient zipClient)
|
||||
{
|
||||
if (logger == null)
|
||||
{
|
||||
@ -94,7 +85,6 @@ namespace Emby.Server.Implementations.Updates
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_zipClient = zipClient;
|
||||
_appConfig = appConfig;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -122,16 +112,14 @@ namespace Emby.Server.Implementations.Updates
|
||||
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<PackageInfo>> GetPackages(string manifest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var manifestUrl = _appConfig.GetValue<string>(PluginManifestUrlKey);
|
||||
|
||||
try
|
||||
{
|
||||
using (var response = await _httpClient.SendAsync(
|
||||
new HttpRequestOptions
|
||||
{
|
||||
Url = manifestUrl,
|
||||
Url = manifest,
|
||||
CancellationToken = cancellationToken,
|
||||
CacheMode = CacheMode.Unconditional,
|
||||
CacheLength = TimeSpan.FromMinutes(3)
|
||||
@ -145,23 +133,38 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
catch (SerializationException ex)
|
||||
{
|
||||
const string LogTemplate =
|
||||
"Failed to deserialize the plugin manifest retrieved from {PluginManifestUrl}. If you " +
|
||||
"have specified a custom plugin repository manifest URL with --plugin-manifest-url or " +
|
||||
PluginManifestUrlKey + ", please ensure that it is correct.";
|
||||
_logger.LogError(ex, LogTemplate, manifestUrl);
|
||||
throw;
|
||||
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (UriFormatException ex)
|
||||
{
|
||||
const string LogTemplate =
|
||||
"The URL configured for the plugin repository manifest URL is not valid: {PluginManifestUrl}. " +
|
||||
"Please check the URL configured by --plugin-manifest-url or " + PluginManifestUrlKey;
|
||||
_logger.LogError(ex, LogTemplate, manifestUrl);
|
||||
throw;
|
||||
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new List<PackageInfo>();
|
||||
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
|
||||
{
|
||||
result.AddRange(await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -402,6 +405,12 @@ namespace Emby.Server.Implementations.Updates
|
||||
/// <param name="plugin">The plugin.</param>
|
||||
public void UninstallPlugin(IPlugin plugin)
|
||||
{
|
||||
if (!plugin.CanUninstall)
|
||||
{
|
||||
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.OnUninstalling();
|
||||
|
||||
// Remove it the quick way for now
|
||||
|
@ -39,21 +39,18 @@ namespace Jellyfin.Api.Auth
|
||||
/// <inheritdoc />
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var authenticatedAttribute = new AuthenticatedAttribute();
|
||||
try
|
||||
{
|
||||
var user = _authService.Authenticate(Request, authenticatedAttribute);
|
||||
if (user == null)
|
||||
var authorizationInfo = _authService.Authenticate(Request);
|
||||
if (authorizationInfo == null)
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
|
||||
}
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, user.Username),
|
||||
new Claim(
|
||||
ClaimTypes.Role,
|
||||
value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
|
||||
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
|
||||
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
|
@ -36,7 +36,6 @@ namespace Jellyfin.Api.Controllers
|
||||
public void CompleteWizard()
|
||||
{
|
||||
_config.Configuration.IsStartupWizardCompleted = true;
|
||||
_config.SetOptimalValues();
|
||||
_config.SaveConfiguration();
|
||||
}
|
||||
|
||||
@ -47,14 +46,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Configuration")]
|
||||
public StartupConfigurationDto GetStartupConfiguration()
|
||||
{
|
||||
var result = new StartupConfigurationDto
|
||||
return new StartupConfigurationDto
|
||||
{
|
||||
UICulture = _config.Configuration.UICulture,
|
||||
MetadataCountryCode = _config.Configuration.MetadataCountryCode,
|
||||
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -93,10 +90,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <returns>The first user.</returns>
|
||||
[HttpGet("User")]
|
||||
public StartupUserDto GetFirstUser()
|
||||
public async Task<StartupUserDto> GetFirstUser()
|
||||
{
|
||||
// TODO: Remove this method when startup wizard no longer requires an existing user.
|
||||
_userManager.Initialize();
|
||||
await _userManager.InitializeAsync().ConfigureAwait(false);
|
||||
var user = _userManager.Users.First();
|
||||
return new StartupUserDto
|
||||
{
|
||||
|
@ -14,9 +14,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -32,17 +32,28 @@ namespace Jellyfin.Data.Entities
|
||||
/// <param name="_personrole1"></param>
|
||||
public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
this.Path = path;
|
||||
|
||||
this.Kind = kind;
|
||||
|
||||
if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
|
||||
if (_metadata0 == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(_metadata0));
|
||||
}
|
||||
|
||||
_metadata0.Artwork.Add(this);
|
||||
|
||||
if (_personrole1 == null) throw new ArgumentNullException(nameof(_personrole1));
|
||||
_personrole1.Artwork = this;
|
||||
if (_personrole1 == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(_personrole1));
|
||||
}
|
||||
|
||||
_personrole1.Artwork = this;
|
||||
|
||||
Init();
|
||||
}
|
||||
@ -87,7 +98,7 @@ namespace Jellyfin.Data.Entities
|
||||
{
|
||||
int value = _Id;
|
||||
GetId(ref value);
|
||||
return (_Id = value);
|
||||
return _Id = value;
|
||||
}
|
||||
|
||||
protected set
|
||||
@ -126,7 +137,7 @@ namespace Jellyfin.Data.Entities
|
||||
{
|
||||
string value = _Path;
|
||||
GetPath(ref value);
|
||||
return (_Path = value);
|
||||
return _Path = value;
|
||||
}
|
||||
|
||||
set
|
||||
@ -163,7 +174,7 @@ namespace Jellyfin.Data.Entities
|
||||
{
|
||||
Enums.ArtKind value = _Kind;
|
||||
GetKind(ref value);
|
||||
return (_Kind = value);
|
||||
return _Kind = value;
|
||||
}
|
||||
|
||||
set
|
||||
|
@ -29,18 +29,30 @@ namespace Jellyfin.Data.Entities
|
||||
/// <summary>
|
||||
/// Public constructor with required data.
|
||||
/// </summary>
|
||||
/// <param name="title">The title or name of the object</param>
|
||||
/// <param name="language">ISO-639-3 3-character language codes</param>
|
||||
/// <param name="title">The title or name of the object.</param>
|
||||
/// <param name="language">ISO-639-3 3-character language codes.</param>
|
||||
/// <param name="_book0"></param>
|
||||
public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
|
||||
{
|
||||
if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(title));
|
||||
}
|
||||
|
||||
this.Title = title;
|
||||
|
||||
if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
|
||||
if (string.IsNullOrEmpty(language))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(language));
|
||||
}
|
||||
|
||||
this.Language = language;
|
||||
|
||||
if (_book0 == null) throw new ArgumentNullException(nameof(_book0));
|
||||
if (_book0 == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(_book0));
|
||||
}
|
||||
|
||||
_book0.BookMetadata.Add(this);
|
||||
|
||||
this.Publishers = new HashSet<Company>();
|
||||
@ -51,8 +63,8 @@ namespace Jellyfin.Data.Entities
|
||||
/// <summary>
|
||||
/// Static create function (for use in LINQ queries, etc.)
|
||||
/// </summary>
|
||||
/// <param name="title">The title or name of the object</param>
|
||||
/// <param name="language">ISO-639-3 3-character language codes</param>
|
||||
/// <param name="title">The title or name of the object.</param>
|
||||
/// <param name="language">ISO-639-3 3-character language codes.</param>
|
||||
/// <param name="_book0"></param>
|
||||
public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
|
||||
{
|
||||
@ -82,7 +94,7 @@ namespace Jellyfin.Data.Entities
|
||||
{
|
||||
long? value = _ISBN;
|
||||
GetISBN(ref value);
|
||||
return (_ISBN = value);
|
||||
return _ISBN = value;
|
||||
}
|
||||
|
||||
set
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user