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

This commit is contained in:
Matt Montgomery 2020-07-26 16:14:58 -05:00
commit a40fe86776
419 changed files with 4830 additions and 2636 deletions

View 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'

View File

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

3
.gitignore vendored
View File

@ -39,7 +39,6 @@ ProgramData*/
CorePlugins*/ CorePlugins*/
ProgramData-Server*/ ProgramData-Server*/
ProgramData-UI*/ ProgramData-UI*/
MediaBrowser.WebDashboard/jellyfin-web/**
################# #################
## Visual Studio ## Visual Studio
@ -276,4 +275,4 @@ BenchmarkDotNet.Artifacts
# Ignore web artifacts from native builds # Ignore web artifacts from native builds
web/ web/
web-src.* web-src.*
MediaBrowser.WebDashboard/jellyfin-web/ MediaBrowser.WebDashboard/jellyfin-web

12
.vscode/launch.json vendored
View File

@ -1,9 +1,6 @@
{ {
// Use IntelliSense to find out which attributes exist for C# debugging "version": "0.2.0",
// Use hover for the description of the existing attributes "configurations": [
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{ {
"name": ".NET Core Launch (console)", "name": ".NET Core Launch (console)",
"type": "coreclr", "type": "coreclr",
@ -24,5 +21,8 @@
"request": "attach", "request": "attach",
"processId": "${command:pickProcess}" "processId": "${command:pickProcess}"
} }
,] ],
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
}
} }

7
.vscode/tasks.json vendored
View File

@ -21,5 +21,10 @@
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
} }
] ],
"options": {
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
}
}
} }

View File

@ -117,12 +117,19 @@ namespace DvdLib.Ifo
uint chapNum = 1; uint chapNum = 1;
vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin); vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1)); var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
if (t == null) continue; if (t == null)
{
continue;
}
do do
{ {
t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum)); 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++; chapNum++;
} }
while (vtsFs.Position < (baseAddr + endaddr)); while (vtsFs.Position < (baseAddr + endaddr));
@ -147,7 +154,10 @@ namespace DvdLib.Ifo
uint vtsPgcOffset = vtsRead.ReadUInt32(); uint vtsPgcOffset = vtsRead.ReadUInt32();
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum)); 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);
}
} }
} }
} }

View File

@ -15,8 +15,14 @@ namespace DvdLib.Ifo
Second = GetBCDValue(data[2]); Second = GetBCDValue(data[2]);
Frames = GetBCDValue((byte)(data[3] & 0x3F)); Frames = GetBCDValue((byte)(data[3] & 0x3F));
if ((data[3] & 0x80) != 0) FrameRate = 30; if ((data[3] & 0x80) != 0)
else if ((data[3] & 0x40) != 0) FrameRate = 25; {
FrameRate = 30;
}
else if ((data[3] & 0x40) != 0)
{
FrameRate = 25;
}
} }
private static byte GetBCDValue(byte data) private static byte GetBCDValue(byte data)

View File

@ -75,8 +75,15 @@ namespace DvdLib.Ifo
StillTime = br.ReadByte(); StillTime = br.ReadByte();
byte pbMode = br.ReadByte(); byte pbMode = br.ReadByte();
if (pbMode == 0) PlaybackMode = ProgramPlaybackMode.Sequential; if (pbMode == 0)
else PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle; {
PlaybackMode = ProgramPlaybackMode.Sequential;
}
else
{
PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
}
ProgramCount = (uint)(pbMode & 0x7F); ProgramCount = (uint)(pbMode & 0x7F);
Palette = br.ReadBytes(64); Palette = br.ReadBytes(64);

View File

@ -59,7 +59,10 @@ namespace DvdLib.Ifo
var pgc = new ProgramChain(pgcNum); var pgc = new ProgramChain(pgcNum);
pgc.ParseHeader(br); pgc.ParseHeader(br);
ProgramChains.Add(pgc); ProgramChains.Add(pgc);
if (entryPgc) EntryProgramChain = pgc; if (entryPgc)
{
EntryProgramChain = pgc;
}
br.BaseStream.Seek(curPos, SeekOrigin.Begin); br.BaseStream.Seek(curPos, SeekOrigin.Begin);
} }

View File

@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Dlna.Api namespace Emby.Dlna.Api
{ {
@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
public string Filename { get; set; } public string Filename { get; set; }
} }
public class DlnaServerService : IService, IRequiresRequest public class DlnaServerService : IService
{ {
private const string XMLContentType = "text/xml; charset=UTF-8"; private const string XMLContentType = "text/xml; charset=UTF-8";
@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
public DlnaServerService( public DlnaServerService(
IDlnaManager dlnaManager, IDlnaManager dlnaManager,
IHttpResultFactory httpResultFactory, IHttpResultFactory httpResultFactory,
IServerConfigurationManager configurationManager) IServerConfigurationManager configurationManager,
IHttpContextAccessor httpContextAccessor)
{ {
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
_resultFactory = httpResultFactory; _resultFactory = httpResultFactory;
_configurationManager = configurationManager; _configurationManager = configurationManager;
Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
} }
private string GetHeader(string name) private string GetHeader(string name)

View File

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

View File

@ -140,55 +140,73 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.DeviceDescription)) if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
{ {
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription)) if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.FriendlyName)) if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
{ {
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)) if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.Manufacturer)) if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{ {
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)) if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl)) if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{ {
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)) if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelDescription)) if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{ {
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)) if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelName)) if (!string.IsNullOrEmpty(profileInfo.ModelName))
{ {
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName)) if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelNumber)) if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{ {
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)) if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelUrl)) if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{ {
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)) if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.SerialNumber)) if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{ {
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber)) if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false; return false;
}
} }
return true; return true;
@ -369,7 +387,7 @@ namespace Emby.Dlna
foreach (var name in _assembly.GetManifestResourceNames()) foreach (var name in _assembly.GetManifestResourceNames())
{ {
if (!name.StartsWith(namespaceName)) if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
{ {
continue; continue;
} }
@ -388,7 +406,7 @@ namespace Emby.Dlna
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
await stream.CopyToAsync(fileStream); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }
} }
} }
@ -491,7 +509,7 @@ namespace Emby.Dlna
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
} }
class InternalProfileInfo private class InternalProfileInfo
{ {
internal DeviceProfileInfo Info { get; set; } internal DeviceProfileInfo Info { get; set; }

View File

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

View File

@ -35,8 +35,6 @@ namespace Emby.Dlna.Main
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly ILogger<DlnaEntryPoint> _logger; private readonly ILogger<DlnaEntryPoint> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private PlayToManager _manager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -47,14 +45,13 @@ namespace Emby.Dlna.Main
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private SsdpDevicePublisher _Publisher;
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer; private ISsdpCommunicationsServer _communicationsServer;
internal IContentDirectory ContentDirectory { get; private set; } internal IContentDirectory ContentDirectory { get; private set; }
@ -181,7 +178,7 @@ namespace Emby.Dlna.Main
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows || var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
OperatingSystem.Id == OperatingSystemId.Linux; OperatingSystem.Id == OperatingSystemId.Linux;
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{ {
IsShared = true IsShared = true
}; };
@ -232,20 +229,22 @@ namespace Emby.Dlna.Main
return; return;
} }
if (_Publisher != null) if (_publisher != null)
{ {
return; return;
} }
try try
{ {
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost); _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
_Publisher.LogFunction = LogMessage; {
_Publisher.SupportPnpRootDevice = false; LogFunction = LogMessage,
SupportPnpRootDevice = false
};
await RegisterServerEndpoints().ConfigureAwait(false); await RegisterServerEndpoints().ConfigureAwait(false);
_Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -267,6 +266,12 @@ namespace Emby.Dlna.Main
continue; continue;
} }
// Limit to LAN addresses only
if (!_networkManager.IsAddressInSubnets(address, true, true))
{
continue;
}
var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
@ -288,13 +293,13 @@ namespace Emby.Dlna.Main
}; };
SetProperies(device, fullService); SetProperies(device, fullService);
_Publisher.AddDevice(device); _publisher.AddDevice(device);
var embeddedDevices = new[] var embeddedDevices = new[]
{ {
"urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ContentDirectory:1",
"urn:schemas-upnp-org:service:ConnectionManager:1", "urn:schemas-upnp-org:service:ConnectionManager:1",
//"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
}; };
foreach (var subDevice in embeddedDevices) foreach (var subDevice in embeddedDevices)
@ -326,7 +331,7 @@ namespace Emby.Dlna.Main
private void SetProperies(SsdpDevice device, string fullDeviceType) 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(':'); var serviceParts = service.Split(':');
@ -337,7 +342,6 @@ namespace Emby.Dlna.Main
device.DeviceType = serviceParts[2]; device.DeviceType = serviceParts[2];
} }
private readonly object _syncLock = new object();
private void StartPlayToManager() private void StartPlayToManager()
{ {
lock (_syncLock) lock (_syncLock)
@ -416,11 +420,11 @@ namespace Emby.Dlna.Main
public void DisposeDevicePublisher() public void DisposeDevicePublisher()
{ {
if (_Publisher != null) if (_publisher != null)
{ {
_logger.LogInformation("Disposing SsdpDevicePublisher"); _logger.LogInformation("Disposing SsdpDevicePublisher");
_Publisher.Dispose(); _publisher.Dispose();
_Publisher = null; _publisher = null;
} }
} }
} }

View File

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -19,8 +19,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class Device : IDisposable public class Device : IDisposable
{ {
#region Fields & Properties
private Timer _timer; private Timer _timer;
public DeviceInfo Properties { get; set; } public DeviceInfo Properties { get; set; }
@ -53,10 +51,10 @@ namespace Emby.Dlna.PlayTo
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED; public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
#endregion
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
public Action OnDeviceUnavailable { get; set; } public Action OnDeviceUnavailable { get; set; }
@ -142,8 +140,6 @@ namespace Emby.Dlna.PlayTo
} }
} }
#region Commanding
public Task VolumeDown(CancellationToken cancellationToken) public Task VolumeDown(CancellationToken cancellationToken)
{ {
var sendVolume = Math.Max(Volume - 5, 0); var sendVolume = Math.Max(Volume - 5, 0);
@ -212,7 +208,9 @@ namespace Emby.Dlna.PlayTo
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null) if (command == null)
{
return false; return false;
}
var service = GetServiceRenderingControl(); var service = GetServiceRenderingControl();
@ -241,7 +239,9 @@ namespace Emby.Dlna.PlayTo
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null) if (command == null)
{
return; return;
}
var service = GetServiceRenderingControl(); var service = GetServiceRenderingControl();
@ -264,7 +264,9 @@ namespace Emby.Dlna.PlayTo
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null) if (command == null)
{
return; return;
}
var service = GetAvTransportService(); var service = GetAvTransportService();
@ -289,7 +291,9 @@ namespace Emby.Dlna.PlayTo
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null) if (command == null)
{
return; return;
}
var dictionary = new Dictionary<string, string> var dictionary = new Dictionary<string, string>
{ {
@ -330,7 +334,7 @@ namespace Emby.Dlna.PlayTo
return string.Empty; return string.Empty;
} }
return DescriptionXmlBuilder.Escape(value); return SecurityElement.Escape(value);
} }
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
@ -402,11 +406,8 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true); RestartTimer(true);
} }
#endregion
#region Get data
private int _connectFailureCount; private int _connectFailureCount;
private async void TimerCallback(object sender) private async void TimerCallback(object sender)
{ {
if (_disposed) if (_disposed)
@ -459,7 +460,9 @@ namespace Emby.Dlna.PlayTo
_connectFailureCount = 0; _connectFailureCount = 0;
if (_disposed) if (_disposed)
{
return; return;
}
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive // 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) if (transportState.Value == TRANSPORTSTATE.STOPPED)
@ -479,7 +482,9 @@ namespace Emby.Dlna.PlayTo
catch (Exception ex) catch (Exception ex)
{ {
if (_disposed) if (_disposed)
{
return; return;
}
_logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name); _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
@ -580,7 +585,9 @@ namespace Emby.Dlna.PlayTo
cancellationToken: cancellationToken).ConfigureAwait(false); cancellationToken: cancellationToken).ConfigureAwait(false);
if (result == null || result.Document == null) if (result == null || result.Document == null)
{
return; return;
}
var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse") var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
.Select(i => i.Element("CurrentMute")) .Select(i => i.Element("CurrentMute"))
@ -870,10 +877,6 @@ namespace Emby.Dlna.PlayTo
return new string[4]; return new string[4];
} }
#endregion
#region From XML
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken) private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
{ {
if (AvCommands != null) if (AvCommands != null)
@ -1068,8 +1071,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClient, logger, config); return new Device(deviceProperties, httpClient, logger, config);
} }
#endregion
private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private static DeviceIcon CreateIcon(XElement element) private static DeviceIcon CreateIcon(XElement element)
{ {
@ -1193,8 +1194,6 @@ namespace Emby.Dlna.PlayTo
}); });
} }
#region IDisposable
bool _disposed; bool _disposed;
public void Dispose() public void Dispose()
@ -1221,8 +1220,6 @@ namespace Emby.Dlna.PlayTo
_disposed = true; _disposed = true;
} }
#endregion
public override string ToString() public override string ToString()
{ {
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);

View File

@ -78,9 +78,15 @@ namespace Emby.Dlna.PlayTo
var info = e.Argument; 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(); string location = info.Location.ToString();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,9 +43,9 @@ using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using Emby.Server.Implementations.SyncPlay;
using MediaBrowser.Api; using MediaBrowser.Api;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -79,8 +79,8 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.TV;
using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
@ -194,7 +194,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the application paths. /// Gets or sets the application paths.
/// </summary> /// </summary>
/// <value>The application paths.</value> /// <value>The application paths.</value>
protected ServerApplicationPaths ApplicationPaths { get; set; } protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
@ -238,7 +238,7 @@ namespace Emby.Server.Implementations
/// Initializes a new instance of the <see cref="ApplicationHost" /> class. /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// </summary> /// </summary>
public ApplicationHost( public ApplicationHost(
ServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IFileSystem fileSystem, IFileSystem fileSystem,
@ -486,12 +486,10 @@ namespace Emby.Server.Implementations
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
pluginBuilder.AppendLine( pluginBuilder.Append(plugin.Name)
string.Format( .Append(' ')
CultureInfo.InvariantCulture, .Append(plugin.Version)
"{0} {1}", .AppendLine();
plugin.Name,
plugin.Version));
} }
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
@ -568,10 +566,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>)); serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
// TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>)); serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
serviceCollection.AddSingleton<IMediaEncoder>(provider => serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@ -802,7 +798,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>()); Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
} }
@ -876,6 +871,11 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
continue; continue;
} }
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
continue;
}
foreach (Type type in exportedTypes) foreach (Type type in exportedTypes)
{ {
@ -1158,7 +1158,7 @@ namespace Emby.Server.Implementations
return null; return null;
} }
return GetLocalApiUrl(addresses.First()); return GetLocalApiUrl(addresses[0]);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1231,13 +1231,13 @@ namespace Emby.Server.Implementations
var addresses = ServerConfigurationManager var addresses = ServerConfigurationManager
.Configuration .Configuration
.LocalNetworkAddresses .LocalNetworkAddresses
.Select(NormalizeConfiguredLocalAddress) .Select(x => NormalizeConfiguredLocalAddress(x))
.Where(i => i != null) .Where(i => i != null)
.ToList(); .ToList();
if (addresses.Count == 0) if (addresses.Count == 0)
{ {
addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); addresses.AddRange(_networkManager.GetLocalIpAddresses());
} }
var resultList = new List<IPAddress>(); var resultList = new List<IPAddress>();
@ -1252,8 +1252,7 @@ namespace Emby.Server.Implementations
} }
} }
var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
if (valid)
{ {
resultList.Add(address); resultList.Add(address);
@ -1267,13 +1266,12 @@ namespace Emby.Server.Implementations
return resultList; return resultList;
} }
public IPAddress NormalizeConfiguredLocalAddress(string address) public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
{ {
var index = address.Trim('/').IndexOf('/'); var index = address.Trim('/').IndexOf('/');
if (index != -1) if (index != -1)
{ {
address = address.Substring(index + 1); address = address.Slice(index + 1);
} }
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))

View File

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

View File

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

View File

@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
{ {
{ HostWebClientKey, bool.TrueString }, { HostWebClientKey, bool.TrueString },
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" }, { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
{ InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" },
{ FfmpegProbeSizeKey, "1G" }, { FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" }, { FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString } { PlaylistsAllowDuplicatesKey, bool.TrueString }

View File

@ -1110,7 +1110,8 @@ namespace Emby.Server.Implementations.Data
continue; continue;
} }
str.Append(ToValueString(i) + "|"); str.Append(ToValueString(i))
.Append('|');
} }
str.Length -= 1; // Remove last | str.Length -= 1; // Remove last |
@ -2471,7 +2472,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo; var item = query.SimilarTo;
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("("); builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating)) if (string.IsNullOrEmpty(item.OfficialRating))
{ {
@ -2509,7 +2510,7 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(query.SearchTerm)) if (!string.IsNullOrEmpty(query.SearchTerm))
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("("); builder.Append('(');
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
@ -2775,22 +2776,85 @@ namespace Emby.Server.Implementations.Data
private string FixUnicodeChars(string buffer) private string FixUnicodeChars(string buffer)
{ {
if (buffer.IndexOf('\u2013') > -1) buffer = buffer.Replace('\u2013', '-'); // en dash if (buffer.IndexOf('\u2013') > -1)
if (buffer.IndexOf('\u2014') > -1) buffer = buffer.Replace('\u2014', '-'); // em dash {
if (buffer.IndexOf('\u2015') > -1) buffer = buffer.Replace('\u2015', '-'); // horizontal bar buffer = buffer.Replace('\u2013', '-'); // en dash
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('\u2014') > -1)
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 buffer = buffer.Replace('\u2014', '-'); // em dash
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('\u2015') > -1)
if (buffer.IndexOf('\u2026') > -1) buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis {
if (buffer.IndexOf('\u2032') > -1) buffer = buffer.Replace('\u2032', '\''); // prime buffer = buffer.Replace('\u2015', '-'); // horizontal bar
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('\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; return buffer;
} }
@ -5175,7 +5239,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{ {
if (i > 0) if (i > 0)
{ {
insertText.Append(","); insertText.Append(',');
} }
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture)); insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
@ -6268,7 +6332,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{ {
insertText.Append("@" + column + index + ","); insertText.Append('@')
.Append(column)
.Append(index)
.Append(',');
} }
insertText.Length -= 1; insertText.Length -= 1;
@ -6308,7 +6375,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
/// Gets the attachment. /// Gets the attachment.
/// </summary> /// </summary>
/// <param name="reader">The reader.</param> /// <param name="reader">The reader.</param>
/// <returns>MediaAttachment</returns> /// <returns>MediaAttachment.</returns>
private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader) private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader)
{ {
var item = new MediaAttachment var item = new MediaAttachment

View File

@ -25,7 +25,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.5.211" /> <PackageReference Include="IPNetwork2" Version="2.5.211" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
@ -34,10 +34,10 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
<PackageReference Include="Mono.Nat" Version="2.0.1" /> <PackageReference Include="Mono.Nat" Version="2.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />

View File

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

View File

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

View File

@ -426,7 +426,7 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options) 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); AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
if (!noCache) if (!noCache)
@ -585,7 +585,7 @@ namespace Emby.Server.Implementations.HttpServer
if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue) 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 OnComplete = options.OnComplete
}; };
@ -622,8 +622,11 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary> /// <summary>
/// Adds the caching responseHeaders. /// Adds the caching responseHeaders.
/// </summary> /// </summary>
private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration, private void AddCachingHeaders(
bool noCache, DateTime? lastModifiedDate) IDictionary<string, string> responseHeaders,
TimeSpan? cacheDuration,
bool noCache,
DateTime? lastModifiedDate)
{ {
if (noCache) if (noCache)
{ {

View File

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -8,52 +9,17 @@ using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer namespace Emby.Server.Implementations.HttpServer
{ {
public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult 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; private const int BufferSize = 81920;
/// <summary>
/// The _options.
/// </summary>
private readonly Dictionary<string, string> _options = new Dictionary<string, string>(); private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
/// <summary> private List<KeyValuePair<long, long?>> _requestedRanges;
/// 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;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RangeRequestWriter" /> class. /// 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="source">The source.</param>
/// <param name="contentType">Type of the content.</param> /// <param name="contentType">Type of the content.</param>
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</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)
public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest, ILogger logger)
{ {
if (string.IsNullOrEmpty(contentType)) if (string.IsNullOrEmpty(contentType))
{ {
@ -74,7 +39,6 @@ namespace Emby.Server.Implementations.HttpServer
RangeHeader = rangeHeader; RangeHeader = rangeHeader;
SourceStream = source; SourceStream = source;
IsHeadRequest = isHeadRequest; IsHeadRequest = isHeadRequest;
this._logger = logger;
ContentType = contentType; ContentType = contentType;
Headers[HeaderNames.ContentType] = contentType; Headers[HeaderNames.ContentType] = contentType;
@ -84,6 +48,81 @@ namespace Emby.Server.Implementations.HttpServer
SetRangeValues(contentLength); 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> /// <summary>
/// Sets the range values. /// Sets the range values.
/// </summary> /// </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) public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{ {
try try
@ -174,59 +169,44 @@ namespace Emby.Server.Implementations.HttpServer
// If the requested range is "0-", we can optimize by just doing a stream copy // If the requested range is "0-", we can optimize by just doing a stream copy
if (RangeEnd >= TotalContentLength - 1) if (RangeEnd >= TotalContentLength - 1)
{ {
await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false); await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
} }
else else
{ {
await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false); await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
} }
} }
} }
finally finally
{ {
if (OnComplete != null) OnComplete?.Invoke();
{
OnComplete();
}
} }
} }
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 = new byte[BufferSize]; var array = ArrayPool<byte>.Shared.Rent(BufferSize);
int bytesRead; try
while ((bytesRead = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
{ {
if (bytesRead == 0) int bytesRead;
while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
{ {
break; var bytesToCopy = Math.Min(bytesRead, copyLength);
}
var bytesToCopy = Math.Min(bytesRead, copyLength); await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false); copyLength -= bytesToCopy;
copyLength -= bytesToCopy; if (copyLength <= 0)
{
if (copyLength <= 0) break;
{ }
break;
} }
} }
} finally
{
public string ContentType { get; set; } ArrayPool<byte>.Shared.Return(array);
}
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;
} }
} }
} }

View File

@ -41,11 +41,11 @@ namespace Emby.Server.Implementations.HttpServer
res.Headers.Add(key, value); res.Headers.Add(key, value);
} }
// Try to prevent compatibility view // 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-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, " + "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, " + "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
"X-Emby-Authorization"); "X-Emby-Authorization";
if (dto is Exception exception) if (dto is Exception exception)
{ {

View File

@ -13,26 +13,22 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer.Security namespace Emby.Server.Implementations.HttpServer.Security
{ {
public class AuthService : IAuthService public class AuthService : IAuthService
{ {
private readonly ILogger<AuthService> _logger;
private readonly IAuthorizationContext _authorizationContext; private readonly IAuthorizationContext _authorizationContext;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
public AuthService( public AuthService(
ILogger<AuthService> logger,
IAuthorizationContext authorizationContext, IAuthorizationContext authorizationContext,
IServerConfigurationManager config, IServerConfigurationManager config,
ISessionManager sessionManager, ISessionManager sessionManager,
INetworkManager networkManager) INetworkManager networkManager)
{ {
_logger = logger;
_authorizationContext = authorizationContext; _authorizationContext = authorizationContext;
_config = config; _config = config;
_sessionManager = sessionManager; _sessionManager = sessionManager;
@ -51,6 +47,22 @@ namespace Emby.Server.Implementations.HttpServer.Security
return user; 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) private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
{ {
// This code is executed before the service // This code is executed before the service

View File

@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security namespace Emby.Server.Implementations.HttpServer.Security
@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(requestContext); return GetAuthorization(requestContext);
} }
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var (authInfo, _) =
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
/// <summary> /// <summary>
/// Gets the authorization. /// Gets the authorization.
/// </summary> /// </summary>
@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(IRequest httpReq) private AuthorizationInfo GetAuthorization(IRequest httpReq)
{ {
var auth = GetAuthorizationDictionary(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 deviceId = null;
string device = null; string device = null;
string client = null; string client = null;
@ -64,20 +89,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
token = httpReq.Headers["X-Emby-Token"]; token = headers["X-Emby-Token"];
} }
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
token = httpReq.Headers["X-MediaBrowser-Token"]; token = headers["X-MediaBrowser-Token"];
} }
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
token = httpReq.QueryString["api_key"]; token = queryString["api_key"];
} }
var info = new AuthorizationInfo var authInfo = new AuthorizationInfo
{ {
Client = client, Client = client,
Device = device, Device = device,
@ -86,6 +111,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
Token = token Token = token
}; };
AuthenticationInfo originalAuthenticationInfo = null;
if (!string.IsNullOrWhiteSpace(token)) if (!string.IsNullOrWhiteSpace(token))
{ {
var result = _authRepo.Get(new AuthenticationInfoQuery var result = _authRepo.Get(new AuthenticationInfoQuery
@ -93,81 +119,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
AccessToken = token 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; var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace // 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 // 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) if (allowTokenInfoUpdate)
{ {
updateToken = true; 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) if (allowTokenInfoUpdate)
{ {
updateToken = true; 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; 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; updateToken = true;
} }
} }
if (updateToken) if (updateToken)
{ {
_authRepo.Update(tokenInfo); _authRepo.Update(originalAuthenticationInfo);
} }
} }
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
} }
httpReq.Items["AuthorizationInfo"] = info; return (authInfo, originalAuthenticationInfo);
return info;
} }
/// <summary> /// <summary>
@ -187,6 +209,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(auth); 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> /// <summary>
/// Gets the authorization. /// Gets the authorization.
/// </summary> /// </summary>

View File

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

View File

@ -36,11 +36,6 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
string RestartArgs { get; } string RestartArgs { get; }
/// <summary>
/// Gets the value of the --plugin-manifest-url command line option.
/// </summary>
string PluginManifestUrl { get; }
/// <summary> /// <summary>
/// Gets the value of the --published-server-url command line option. /// Gets the value of the --published-server-url command line option.
/// </summary> /// </summary>

View File

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

View File

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

View File

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

View File

@ -1,3 +1,6 @@
#nullable enable
using System;
using System.Linq; using System.Linq;
using DotNet.Globbing; using DotNet.Globbing;
@ -11,7 +14,7 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Files matching these glob patterns will be ignored. /// Files matching these glob patterns will be ignored.
/// </summary> /// </summary>
public static readonly string[] Patterns = new string[] private static readonly string[] _patterns =
{ {
"**/small.jpg", "**/small.jpg",
"**/albumart.jpg", "**/albumart.jpg",
@ -19,32 +22,51 @@ namespace Emby.Server.Implementations.Library
// Directories // Directories
"**/metadata/**", "**/metadata/**",
"**/metadata",
"**/ps3_update/**", "**/ps3_update/**",
"**/ps3_update",
"**/ps3_vprm/**", "**/ps3_vprm/**",
"**/ps3_vprm",
"**/extrafanart/**", "**/extrafanart/**",
"**/extrafanart",
"**/extrathumbs/**", "**/extrathumbs/**",
"**/extrathumbs",
"**/.actors/**", "**/.actors/**",
"**/.actors",
"**/.wd_tv/**", "**/.wd_tv/**",
"**/.wd_tv",
"**/lost+found/**", "**/lost+found/**",
"**/lost+found",
// WMC temp recording directories that will constantly be written to // WMC temp recording directories that will constantly be written to
"**/TempRec/**", "**/TempRec/**",
"**/TempRec",
"**/TempSBE/**", "**/TempSBE/**",
"**/TempSBE",
// Synology // Synology
"**/eaDir/**", "**/eaDir/**",
"**/eaDir",
"**/@eaDir/**", "**/@eaDir/**",
"**/@eaDir",
"**/#recycle/**", "**/#recycle/**",
"**/#recycle",
// Qnap // Qnap
"**/@Recycle/**", "**/@Recycle/**",
"**/@Recycle",
"**/.@__thumb/**", "**/.@__thumb/**",
"**/.@__thumb",
"**/$RECYCLE.BIN/**", "**/$RECYCLE.BIN/**",
"**/$RECYCLE.BIN",
"**/System Volume Information/**", "**/System Volume Information/**",
"**/System Volume Information",
"**/.grab/**", "**/.grab/**",
"**/.grab",
// Unix hidden files and directories // Unix hidden files and directories
"**/.*/**", "**/.*/**",
"**/.*",
// thumbs.db // thumbs.db
"**/thumbs.db", "**/thumbs.db",
@ -56,19 +78,31 @@ namespace Emby.Server.Implementations.Library
private static readonly GlobOptions _globOptions = new GlobOptions private static readonly GlobOptions _globOptions = new GlobOptions
{ {
Evaluation = { Evaluation =
{
CaseInsensitive = true CaseInsensitive = true
} }
}; };
private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray(); private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
/// <summary> /// <summary>
/// Returns true if the supplied path should be ignored. /// Returns true if the supplied path should be ignored.
/// </summary> /// </summary>
public static bool ShouldIgnore(string path) /// <param name="path">The path to test.</param>
/// <returns>Whether to ignore the path.</returns>
public static bool ShouldIgnore(ReadOnlySpan<char> path)
{ {
return _globs.Any(g => g.IsMatch(path)); int len = _globs.Length;
for (int i = 0; i < len; i++)
{
if (_globs[i].IsMatch(path))
{
return true;
}
}
return false;
} }
} }
} }

View File

@ -60,6 +60,8 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger; private readonly ILogger<LibraryManager> _logger;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -75,68 +77,29 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
/// <summary>
/// The _root folder sync lock.
/// </summary>
private readonly object _rootFolderSyncLock = new object();
private readonly object _userRootFolderSyncLock = new object();
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
private NamingOptions _namingOptions; private NamingOptions _namingOptions;
private string[] _videoFileExtensions; private string[] _videoFileExtensions;
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
private IProviderManager ProviderManager => _providerManagerFactory.Value;
private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
/// <summary> /// <summary>
/// Gets or sets the postscan tasks. /// The _root folder.
/// </summary> /// </summary>
/// <value>The postscan tasks.</value> private volatile AggregateFolder _rootFolder;
private ILibraryPostScanTask[] PostscanTasks { get; set; } private volatile UserRootFolder _userRootFolder;
/// <summary> private bool _wizardCompleted;
/// 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; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class. /// Initializes a new instance of the <see cref="LibraryManager" /> class.
/// </summary> /// </summary>
/// <param name="appHost">The application host</param> /// <param name="appHost">The application host.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="taskManager">The task manager.</param> /// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param> /// <param name="userManager">The user manager.</param>
@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Adds the parts. /// Occurs when [item added].
/// </summary> /// </summary>
/// <param name="rules">The rules.</param> public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <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> /// <summary>
/// The _root folder. /// Occurs when [item updated].
/// </summary> /// </summary>
private volatile AggregateFolder _rootFolder; public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary> /// <summary>
/// The _root folder sync lock. /// Occurs when [item removed].
/// </summary> /// </summary>
private readonly object _rootFolderSyncLock = new object(); public event EventHandler<ItemChangeEventArgs> ItemRemoved;
/// <summary> /// <summary>
/// Gets the root folder. /// 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> /// <summary>
/// Records the configuration values. /// Records the configuration values.
@ -341,7 +347,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram) if (item is LiveTvProgram)
{ {
_logger.LogDebug( _logger.LogDebug(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name, item.GetType().Name,
item.Name ?? "Unknown name", item.Name ?? "Unknown name",
item.Path ?? string.Empty, item.Path ?? string.Empty,
@ -350,7 +356,7 @@ namespace Emby.Server.Implementations.Library
else else
{ {
_logger.LogInformation( _logger.LogInformation(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name, item.GetType().Name,
item.Name ?? "Unknown name", item.Name ?? "Unknown name",
item.Path ?? string.Empty, item.Path ?? string.Empty,
@ -368,7 +374,12 @@ namespace Emby.Server.Implementations.Library
continue; continue;
} }
_logger.LogDebug("Deleting path {MetadataPath}", metadataPath); _logger.LogDebug(
"Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
item.Id);
try try
{ {
@ -392,7 +403,13 @@ namespace Emby.Server.Implementations.Library
{ {
try try
{ {
_logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName); _logger.LogInformation(
"Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
if (fileSystemInfo.IsDirectory) if (fileSystemInfo.IsDirectory)
{ {
Directory.Delete(fileSystemInfo.FullName, true); Directory.Delete(fileSystemInfo.FullName, true);
@ -500,8 +517,8 @@ namespace Emby.Server.Implementations.Library
{ {
// Try to normalize paths located underneath program-data in an attempt to make them more portable // Try to normalize paths located underneath program-data in an attempt to make them more portable
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length) key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
.TrimStart(new[] { '/', '\\' }) .TrimStart('/', '\\')
.Replace("/", "\\"); .Replace('/', '\\');
} }
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds) if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@ -514,8 +531,8 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5(); return key.GetMD5();
} }
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, bool allowIgnorePath = true) public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
=> ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent, allowIgnorePath: allowIgnorePath); => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
private BaseItem ResolvePath( private BaseItem ResolvePath(
FileSystemMetadata fileInfo, FileSystemMetadata fileInfo,
@ -523,8 +540,7 @@ namespace Emby.Server.Implementations.Library
IItemResolver[] resolvers, IItemResolver[] resolvers,
Folder parent = null, Folder parent = null,
string collectionType = null, string collectionType = null,
LibraryOptions libraryOptions = null, LibraryOptions libraryOptions = null)
bool allowIgnorePath = true)
{ {
if (fileInfo == null) if (fileInfo == null)
{ {
@ -548,7 +564,7 @@ namespace Emby.Server.Implementations.Library
}; };
// Return null if ignore rules deem that we should do so // Return null if ignore rules deem that we should do so
if (allowIgnorePath && IgnoreFile(args.FileInfo, args.Parent)) if (IgnoreFile(args.FileInfo, args.Parent))
{ {
return null; return null;
} }
@ -713,7 +729,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(rootFolderPath); Directory.CreateDirectory(rootFolderPath);
var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath), allowIgnorePath: false)) ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
.DeepCopy<Folder, AggregateFolder>(); .DeepCopy<Folder, AggregateFolder>();
// In case program data folder was moved // In case program data folder was moved
@ -765,14 +781,11 @@ namespace Emby.Server.Implementations.Library
return rootFolder; return rootFolder;
} }
private volatile UserRootFolder _userRootFolder;
private readonly object _syncLock = new object();
public Folder GetUserRootFolder() public Folder GetUserRootFolder()
{ {
if (_userRootFolder == null) if (_userRootFolder == null)
{ {
lock (_syncLock) lock (_userRootFolderSyncLock)
{ {
if (_userRootFolder == null) if (_userRootFolder == null)
{ {
@ -795,7 +808,7 @@ namespace Emby.Server.Implementations.Library
if (tmpItem == null) if (tmpItem == null)
{ {
_logger.LogDebug("Creating new userRootFolder with DeepCopy"); _logger.LogDebug("Creating new userRootFolder with DeepCopy");
tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath), allowIgnorePath: false)).DeepCopy<Folder, UserRootFolder>(); tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
} }
// In case program data folder was moved // In case program data folder was moved
@ -1322,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
return new QueryResult<BaseItem> return new QueryResult<BaseItem>
{ {
Items = _itemRepository.GetItemList(query).ToArray() Items = _itemRepository.GetItemList(query)
}; };
} }
@ -1453,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItems(query); return _itemRepository.GetItems(query);
} }
var list = _itemRepository.GetItemList(query);
return new QueryResult<BaseItem> return new QueryResult<BaseItem>
{ {
Items = list Items = _itemRepository.GetItemList(query)
}; };
} }
@ -1793,7 +1804,7 @@ namespace Emby.Server.Implementations.Library
/// Creates the items. /// Creates the items.
/// </summary> /// </summary>
/// <param name="items">The items.</param> /// <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> /// <param name="cancellationToken">The cancellation token.</param>
public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken) 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(); var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
if (outdated.Length == 0) // Skip image processing if current or live tv source
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{ {
RegisterItem(item); RegisterItem(item);
return; return;
@ -1894,9 +1906,19 @@ namespace Emby.Server.Implementations.Library
} }
} }
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image); try
image.Width = size.Width; {
image.Height = size.Height; ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
image.Width = size.Width;
image.Height = size.Height;
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path);
image.Width = 0;
image.Height = 0;
continue;
}
try try
{ {
@ -1925,12 +1947,9 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Updates the item. /// Updates the item.
/// </summary> /// </summary>
public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{ {
// Don't iterate multiple times foreach (var item in items)
var itemsList = items.ToList();
foreach (var item in itemsList)
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
{ {
@ -1942,11 +1961,11 @@ namespace Emby.Server.Implementations.Library
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
} }
_itemRepository.SaveItems(itemsList, cancellationToken); _itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null) if (ItemUpdated != null)
{ {
foreach (var item in itemsList) foreach (var item in items)
{ {
// With the live tv guide this just creates too much noise // With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library) if (item.SourceType != SourceType.Library)
@ -2169,8 +2188,6 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => !string.IsNullOrEmpty(i)); .FirstOrDefault(i => !string.IsNullOrEmpty(i));
} }
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
public UserView GetNamedView( public UserView GetNamedView(
User user, User user,
string name, string name,
@ -2468,14 +2485,9 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
var episodeInfo = episode.IsFileProtocol ? var episodeInfo = episode.IsFileProtocol
resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) : ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
new Naming.TV.EpisodeInfo(); : new Naming.TV.EpisodeInfo();
if (episodeInfo == null)
{
episodeInfo = new Naming.TV.EpisodeInfo();
}
try try
{ {
@ -2483,11 +2495,13 @@ namespace Emby.Server.Implementations.Library
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{ {
// Read from metadata // Read from metadata
var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest var mediaInfo = _mediaEncoder.GetMediaInfo(
{ new MediaInfoRequest
MediaSource = episode.GetMediaSources(false)[0], {
MediaType = DlnaProfileType.Video MediaSource = episode.GetMediaSources(false)[0],
}, CancellationToken.None).GetAwaiter().GetResult(); MediaType = DlnaProfileType.Video
},
CancellationToken.None).GetAwaiter().GetResult();
if (mediaInfo.ParentIndexNumber > 0) if (mediaInfo.ParentIndexNumber > 0)
{ {
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@ -2645,7 +2659,7 @@ namespace Emby.Server.Implementations.Library
var videos = videoListResolver.Resolve(fileSystemChildren); var videos = videoListResolver.Resolve(fileSystemChildren);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase)); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null) if (currentVideo != null)
{ {
@ -2662,9 +2676,7 @@ namespace Emby.Server.Implementations.Library
.Select(video => .Select(video =>
{ {
// Try to retrieve it from the db. If we don't find it, use the resolved version // Try to retrieve it from the db. If we don't find it, use the resolved version
var dbItem = GetItemById(video.Id) as Trailer; if (GetItemById(video.Id) is Trailer dbItem)
if (dbItem != null)
{ {
video = dbItem; video = dbItem;
} }
@ -2897,7 +2909,8 @@ namespace Emby.Server.Implementations.Library
} }
catch (HttpException ex) 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; 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) public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{ {
AddMediaPathInternal(virtualFolderName, pathInfo, true); AddMediaPathInternal(virtualFolderName, pathInfo, true);
@ -3031,11 +3027,6 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The path does not exist."); throw new FileNotFoundException("The path does not exist.");
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3074,11 +3065,6 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(pathInfo)); throw new ArgumentNullException(nameof(pathInfo));
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3210,7 +3196,8 @@ namespace Emby.Server.Implementations.Library
if (!Directory.Exists(virtualFolderPath)) if (!Directory.Exists(virtualFolderPath))
{ {
throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName)); throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
} }
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)

View File

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

View File

@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
{ {
public class MediaSourceManager : IMediaSourceManager, IDisposable public class MediaSourceManager : IMediaSourceManager, IDisposable
{ {
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char LiveStreamIdDelimeter = '_';
private readonly IItemRepository _itemRepo; private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -40,6 +43,11 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private readonly object _disposeLock = new object();
private IMediaSourceProvider[] _providers; private IMediaSourceProvider[] _providers;
public MediaSourceManager( public MediaSourceManager(
@ -368,7 +376,6 @@ namespace Emby.Server.Implementations.Library
} }
} }
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
@ -451,9 +458,6 @@ namespace Emby.Server.Implementations.Library
.ToList(); .ToList();
} }
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken) public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{ {
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@ -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) private Tuple<IMediaSourceProvider, string> GetProvider(string key)
{ {
if (string.IsNullOrEmpty(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 provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
var splitIndex = key.IndexOf(LiveStreamIdDelimeter); var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1); var keyId = key.Substring(splitIndex + 1);
return new Tuple<IMediaSourceProvider, string>(provider, keyId); return new Tuple<IMediaSourceProvider, string>(provider, keyId);
@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this);
} }
private readonly object _disposeLock = new object();
/// <summary> /// <summary>
/// Releases unmanaged and - optionally - managed resources. /// Releases unmanaged and - optionally - managed resources.
/// </summary> /// </summary>

View File

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

View File

@ -19,7 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
// Only process items that are in a collection folder containing books // Only process items that are in a collection folder containing books
if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
{
return null; return null;
}
if (args.IsDirectory) 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 // Don't return a Book if there is more (or less) than one document in the directory
if (bookFiles.Count != 1) if (bookFiles.Count != 1)
{
return null; return null;
}
return new Book return new Book
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -195,13 +195,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
while (!sr.EndOfStream) while (!sr.EndOfStream)
{ {
string line = StripXML(sr.ReadLine()); string line = StripXML(sr.ReadLine());
if (line.Contains("Channel")) if (line.Contains("Channel", StringComparison.Ordinal))
{ {
LiveTvTunerStatus status; LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = line.Substring(0, index - 1); var name = line.Substring(0, index - 1);
var currentChannel = line.Substring(index + 7); 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 tuners.Add(new LiveTvTunerInfo
{ {
@ -219,6 +226,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static string StripXML(string source) private static string StripXML(string source)
{ {
if (string.IsNullOrEmpty(source))
{
return string.Empty;
}
char[] buffer = new char[source.Length]; char[] buffer = new char[source.Length];
int bufferIndex = 0; int bufferIndex = 0;
bool inside = false; bool inside = false;
@ -263,7 +275,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
for (int i = 0; i < model.TunerCount; ++i) for (int i = 0; i < model.TunerCount; ++i)
{ {
var name = string.Format("Tuner {0}", i + 1); var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
var currentChannel = "none"; // @todo Get current channel and map back to Station Id var currentChannel = "none"; // @todo Get current channel and map back to Station Id
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
@ -691,7 +703,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
var model = ModelNumber ?? string.Empty; var model = ModelNumber ?? string.Empty;
if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
{ {
return true; return true;
} }

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}", "AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung", "Application": "Anwendung",
"Artists": "Interpreten", "Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert", "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
"Books": "Bücher", "Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
"Channels": "Kanäle", "Channels": "Kanäle",

View File

@ -20,7 +20,7 @@
"HeaderContinueWatching": "Seguir viendo", "HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteEpisodes": "Capítulos favoritos",
"HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas", "HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo", "HeaderLiveTV": "TV en vivo",

View File

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

View File

@ -1,5 +1,5 @@
{ {
"LabelRunningTimeValue": "Duración: {0}", "LabelRunningTimeValue": "Tiempo en ejecución: {0}",
"ValueSpecialEpisodeName": "Especial - {0}", "ValueSpecialEpisodeName": "Especial - {0}",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"Songs": "Canciones", "Songs": "Canciones",

View File

@ -5,23 +5,23 @@
"Artists": "Izvođači", "Artists": "Izvođači",
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena", "AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
"Books": "Knjige", "Books": "Knjige",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}", "ChapterNameValue": "Poglavlje {0}",
"Collections": "Kolekcije", "Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} se odspojilo", "DeviceOfflineWithName": "{0} se odspojilo",
"DeviceOnlineWithName": "{0} je spojeno", "DeviceOnlineWithName": "{0} je spojeno",
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}", "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
"Favorites": "Omiljeni", "Favorites": "Favoriti",
"Folders": "Mape", "Folders": "Mape",
"Genres": "Žanrovi", "Genres": "Žanrovi",
"HeaderAlbumArtists": "Izvođači albuma", "HeaderAlbumArtists": "Izvođači na albumu",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Uvoz sa kamere",
"HeaderContinueWatching": "Continue Watching", "HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi", "HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači", "HeaderFavoriteArtists": "Omiljeni izvođači",
"HeaderFavoriteEpisodes": "Omiljene epizode", "HeaderFavoriteEpisodes": "Omiljene epizode",
"HeaderFavoriteShows": "Omiljene emisije", "HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme", "HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo", "HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Sljedeće je", "HeaderNextUp": "Sljedeće je",
@ -34,23 +34,23 @@
"LabelRunningTimeValue": "Vrijeme rada: {0}", "LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije", "Latest": "Najnovije",
"MessageApplicationUpdated": "Jellyfin Server je ažuriran", "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", "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane", "MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
"MixedContent": "Miješani sadržaj", "MixedContent": "Miješani sadržaj",
"Movies": "Filmovi", "Movies": "Filmovi",
"Music": "Glazba", "Music": "Glazba",
"MusicVideos": "Glazbeni spotovi", "MusicVideos": "Glazbeni spotovi",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "{0} neuspješnih instalacija",
"NameSeasonNumber": "Sezona {0}", "NameSeasonNumber": "Sezona {0}",
"NameSeasonUnknown": "Season Unknown", "NameSeasonUnknown": "Nepoznata sezona",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije", "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije", "NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta", "NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena", "NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete", "NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
"NotificationOptionInstallationFailed": "Instalacija nije izvršena", "NotificationOptionInstallationFailed": "Instalacija neuspješna",
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan", "NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
"NotificationOptionPluginError": "Dodatak otkazao", "NotificationOptionPluginError": "Dodatak otkazao",
"NotificationOptionPluginInstalled": "Dodatak instaliran", "NotificationOptionPluginInstalled": "Dodatak instaliran",
@ -62,7 +62,7 @@
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta", "NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena", "NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
"Photos": "Slike", "Photos": "Slike",
"Playlists": "Popisi", "Playlists": "Popis za reprodukciju",
"Plugin": "Dodatak", "Plugin": "Dodatak",
"PluginInstalledWithName": "{0} je instalirano", "PluginInstalledWithName": "{0} je instalirano",
"PluginUninstalledWithName": "{0} je deinstalirano", "PluginUninstalledWithName": "{0} je deinstalirano",
@ -70,15 +70,15 @@
"ProviderValue": "Pružitelj: {0}", "ProviderValue": "Pružitelj: {0}",
"ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto", "ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
"Shows": "Shows", "Shows": "Serije",
"Songs": "Pjesme", "Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.", "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}", "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.", "Sync": "Sink.",
"System": "Sistem", "System": "Sistem",
"TvShows": "TV Shows", "TvShows": "Serije",
"User": "Korisnik", "User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je stvoren", "UserCreatedWithName": "Korisnik {0} je stvoren",
"UserDeletedWithName": "Korisnik {0} je obrisan", "UserDeletedWithName": "Korisnik {0} je obrisan",
@ -87,10 +87,10 @@
"UserOfflineFromDevice": "{0} se odspojilo od {1}", "UserOfflineFromDevice": "{0} se odspojilo od {1}",
"UserOnlineFromDevice": "{0} je online od {1}", "UserOnlineFromDevice": "{0} je online od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", "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}", "UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {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}", "ValueSpecialEpisodeName": "Specijal - {0}",
"VersionNumber": "Verzija {0}", "VersionNumber": "Verzija {0}",
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.", "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.", "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
"TaskCleanCache": "Očisti priručnu memoriju", "TaskCleanCache": "Očisti priručnu memoriju",
"TasksApplicationCategory": "Aplikacija", "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"
} }

View File

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

View File

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

View File

@ -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": "भिडियो प्लेब्याक सुरु भयो"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,10 @@ namespace Emby.Server.Implementations.Net
public UdpSocket(Socket socket, int localPort, IPAddress ip) 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; _socket = socket;
_localPort = localPort; _localPort = localPort;
@ -103,7 +106,10 @@ namespace Emby.Server.Implementations.Net
public UdpSocket(Socket socket, IPEndPoint endPoint) 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 = socket;
_socket.Connect(endPoint); _socket.Connect(endPoint);

View File

@ -2,7 +2,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Networking namespace Emby.Server.Implementations.Networking
{ {
/// <summary>
/// Class to take care of network interface management.
/// </summary>
public class NetworkManager : INetworkManager public class NetworkManager : INetworkManager
{ {
private readonly ILogger<NetworkManager> _logger; private readonly ILogger<NetworkManager> _logger;
@ -21,8 +23,14 @@ namespace Emby.Server.Implementations.Networking
private readonly object _localIpAddressSyncLock = new object(); private readonly object _localIpAddressSyncLock = new object();
private readonly object _subnetLookupLock = 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) public NetworkManager(ILogger<NetworkManager> logger)
{ {
_logger = logger; _logger = logger;
@ -31,8 +39,10 @@ namespace Emby.Server.Implementations.Networking
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
} }
/// <inheritdoc/>
public event EventHandler NetworkChanged; public event EventHandler NetworkChanged;
/// <inheritdoc/>
public Func<string[]> LocalSubnetsFn { get; set; } public Func<string[]> LocalSubnetsFn { get; set; }
private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e) private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
@ -58,13 +68,14 @@ namespace Emby.Server.Implementations.Networking
NetworkChanged?.Invoke(this, EventArgs.Empty); NetworkChanged?.Invoke(this, EventArgs.Empty);
} }
public IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface = true) /// <inheritdoc/>
public IPAddress[] GetLocalIpAddresses()
{ {
lock (_localIpAddressSyncLock) lock (_localIpAddressSyncLock)
{ {
if (_localIpAddresses == null) if (_localIpAddresses == null)
{ {
var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).ToArray(); var addresses = GetLocalIpAddressesInternal().ToArray();
_localIpAddresses = addresses; _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) if (list.Count == 0)
{ {
list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); 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) .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1)
.ThenBy(i => listClone.IndexOf(i)) // .ThenBy(i => listClone.IndexOf(i))
.Where(FilterIpAddress)
.GroupBy(i => i.ToString()) .GroupBy(i => i.ToString())
.Select(x => x.First()) .Select(x => x.First())
.ToList(); .ToList();
} }
private static bool FilterIpAddress(IPAddress address) /// <inheritdoc/>
{
if (address.IsIPv6LinkLocal
|| address.ToString().StartsWith("169.", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
public bool IsInPrivateAddressSpace(string endpoint) public bool IsInPrivateAddressSpace(string endpoint)
{ {
return IsInPrivateAddressSpace(endpoint, true); return IsInPrivateAddressSpace(endpoint, true);
} }
// Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address
private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets)
{ {
if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase))
@ -116,12 +132,12 @@ namespace Emby.Server.Implementations.Networking
return true; return true;
} }
// ipv6 // IPV6
if (endpoint.Split('.').Length > 4) if (endpoint.Split('.').Length > 4)
{ {
// Handle ipv4 mapped to ipv6 // Handle ipv4 mapped to ipv6
var originalEndpoint = endpoint; var originalEndpoint = endpoint;
endpoint = endpoint.Replace("::ffff:", string.Empty); endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase);
if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase)) if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase))
{ {
@ -130,23 +146,26 @@ namespace Emby.Server.Implementations.Networking
} }
// Private address space: // Private address space:
// http://en.wikipedia.org/wiki/Private_network
if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase)) if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase))
{
return Is172AddressPrivate(endpoint);
}
if (endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase))
{ {
return true; 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)) if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
@ -157,6 +176,7 @@ namespace Emby.Server.Implementations.Networking
return false; return false;
} }
/// <inheritdoc/>
public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint) public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint)
{ {
if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase)) if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase))
@ -179,6 +199,7 @@ namespace Emby.Server.Implementations.Networking
return false; return false;
} }
// Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart
private List<string> GetSubnets(string endpointFirstPart) private List<string> GetSubnets(string endpointFirstPart)
{ {
lock (_subnetLookupLock) lock (_subnetLookupLock)
@ -224,46 +245,81 @@ namespace Emby.Server.Implementations.Networking
} }
} }
private static bool Is172AddressPrivate(string endpoint) /// <inheritdoc/>
{
for (var i = 16; i <= 31; i++)
{
if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public bool IsInLocalNetwork(string endpoint) public bool IsInLocalNetwork(string endpoint)
{ {
return IsInLocalNetworkInternal(endpoint, true); return IsInLocalNetworkInternal(endpoint, true);
} }
/// <inheritdoc/>
public bool IsAddressInSubnets(string addressString, string[] subnets) public bool IsAddressInSubnets(string addressString, string[] subnets)
{ {
return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, 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) private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets)
{ {
foreach (var subnet in subnets) foreach (var subnet in subnets)
{ {
var normalizedSubnet = subnet.Trim(); 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)) if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase))
{ {
return true; return true;
} }
// Parse CIDR subnets and see if address falls within it.
if (normalizedSubnet.Contains('/', StringComparison.Ordinal)) if (normalizedSubnet.Contains('/', StringComparison.Ordinal))
{ {
var ipNetwork = IPNetwork.Parse(normalizedSubnet); try
if (ipNetwork.Contains(address))
{ {
return true; var ipNetwork = IPNetwork.Parse(normalizedSubnet);
if (ipNetwork.Contains(address))
{
return true;
}
}
catch
{
// Ignoring - invalid subnet passed encountered.
} }
} }
} }
@ -288,7 +344,7 @@ namespace Emby.Server.Implementations.Networking
var localSubnets = localSubnetsFn(); var localSubnets = localSubnetsFn();
foreach (var subnet in localSubnets) 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)) if (!string.IsNullOrWhiteSpace(subnet))
{ {
return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false); return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false);
@ -334,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
var host = uri.DnsSafeHost; var host = uri.DnsSafeHost;
_logger.LogDebug("Resolving host {0}", host); _logger.LogDebug("Resolving host {0}", host);
address = GetIpAddresses(host).Result.FirstOrDefault(); address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
if (address != null) if (address != null)
{ {
@ -345,7 +401,7 @@ namespace Emby.Server.Implementations.Networking
} }
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
// Can happen with reverse proxy or IIS url rewriting // Can happen with reverse proxy or IIS url rewriting?
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -362,7 +418,7 @@ namespace Emby.Server.Implementations.Networking
return Dns.GetHostAddressesAsync(hostName); return Dns.GetHostAddressesAsync(hostName);
} }
private IEnumerable<IPAddress> GetIPsDefault(bool ignoreVirtualInterface) private IEnumerable<IPAddress> GetIPsDefault()
{ {
IEnumerable<NetworkInterface> interfaces; IEnumerable<NetworkInterface> interfaces;
@ -382,15 +438,7 @@ namespace Emby.Server.Implementations.Networking
{ {
var ipProperties = network.GetIPProperties(); var ipProperties = network.GetIPProperties();
// Try to exclude virtual adapters // Exclude any addresses if they appear in the LAN list in [ ]
// 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>();
}
return ipProperties.UnicastAddresses return ipProperties.UnicastAddresses
.Select(i => i.Address) .Select(i => i.Address)
@ -423,33 +471,29 @@ namespace Emby.Server.Implementations.Networking
return port; return port;
} }
/// <inheritdoc/>
public int GetRandomUnusedUdpPort() public int GetRandomUnusedUdpPort()
{ {
var localEndPoint = new IPEndPoint(IPAddress.Any, 0); var localEndPoint = new IPEndPoint(IPAddress.Any, 0);
using (var udpClient = new UdpClient(localEndPoint)) using (var udpClient = new UdpClient(localEndPoint))
{ {
var port = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
return port;
} }
} }
private List<PhysicalAddress> _macAddresses; /// <inheritdoc/>
public List<PhysicalAddress> GetMacAddresses() public List<PhysicalAddress> GetMacAddresses()
{ {
if (_macAddresses == null) return _macAddresses ??= GetMacAddressesInternal().ToList();
{
_macAddresses = GetMacAddressesInternal().ToList();
}
return _macAddresses;
} }
private static IEnumerable<PhysicalAddress> GetMacAddressesInternal() private static IEnumerable<PhysicalAddress> GetMacAddressesInternal()
=> NetworkInterface.GetAllNetworkInterfaces() => NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback) .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Select(x => x.GetPhysicalAddress()) .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) public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask)
{ {
IPAddress network1 = GetNetworkAddress(address1, subnetMask); IPAddress network1 = GetNetworkAddress(address1, subnetMask);
@ -476,6 +520,7 @@ namespace Emby.Server.Implementations.Networking
return new IPAddress(broadcastAddress); return new IPAddress(broadcastAddress);
} }
/// <inheritdoc/>
public IPAddress GetLocalIpSubnetMask(IPAddress address) public IPAddress GetLocalIpSubnetMask(IPAddress address)
{ {
NetworkInterface[] interfaces; NetworkInterface[] interfaces;
@ -496,14 +541,11 @@ namespace Emby.Server.Implementations.Networking
foreach (NetworkInterface ni in interfaces) foreach (NetworkInterface ni in interfaces)
{ {
if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null) foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
{ {
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) if (ip.Address.Equals(address) && ip.IPv4Mask != null)
{ {
if (ip.Address.Equals(address) && ip.IPv4Mask != null) return ip.IPv4Mask;
{
return ip.IPv4Mask;
}
} }
} }
} }

View File

@ -539,13 +539,21 @@ namespace Emby.Server.Implementations.Playlists
private static string UnEscape(string content) private static string UnEscape(string content)
{ {
if (content == null) return content; if (content == null)
{
return content;
}
return content.Replace("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<"); return content.Replace("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<");
} }
private static string Escape(string content) private static string Escape(string content)
{ {
if (content == null) return null; if (content == null)
{
return null;
}
return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;"); return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;");
} }

View File

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

View File

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

View File

@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Security
statement.TryBind("@AppName", info.AppName); statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion); statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName); 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("@UserName", info.UserName);
statement.TryBind("@IsActive", true); statement.TryBind("@IsActive", true);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.Security
statement.TryBind("@AppName", info.AppName); statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion); statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName); 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("@UserName", info.UserName);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue()); statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());

View File

@ -40,7 +40,9 @@ namespace Emby.Server.Implementations.Services
if (httpResult != null) if (httpResult != null)
{ {
if (httpResult.RequestContext == null) if (httpResult.RequestContext == null)
{
httpResult.RequestContext = request; httpResult.RequestContext = request;
}
response.StatusCode = httpResult.Status; response.StatusCode = httpResult.Status;
} }

View File

@ -144,7 +144,10 @@ namespace Emby.Server.Implementations.Services
var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts); var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
foreach (var potentialHashMatch in yieldedWildcardMatches) foreach (var potentialHashMatch in yieldedWildcardMatches)
{ {
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue; if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
{
continue;
}
var bestScore = -1; var bestScore = -1;
RestPath bestMatch = null; RestPath bestMatch = null;
@ -186,5 +189,4 @@ namespace Emby.Server.Implementations.Services
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
} }
} }
} }

View File

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
@ -42,11 +43,15 @@ namespace Emby.Server.Implementations.Services
} }
if (mi.GetParameters().Length != 1) if (mi.GetParameters().Length != 1)
{
continue; continue;
}
var actionName = mi.Name; var actionName = mi.Name;
if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase)) if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
{
continue; continue;
}
list.Add(mi); list.Add(mi);
} }
@ -63,7 +68,10 @@ namespace Emby.Server.Implementations.Services
{ {
foreach (var actionCtx in actions) foreach (var actionCtx in actions)
{ {
if (execMap.ContainsKey(actionCtx.Id)) continue; if (execMap.ContainsKey(actionCtx.Id))
{
continue;
}
execMap[actionCtx.Id] = actionCtx; execMap[actionCtx.Id] = actionCtx;
} }
@ -98,7 +106,13 @@ namespace Emby.Server.Implementations.Services
} }
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant(); 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) private static async Task<object> GetTaskResult(Task task)

View File

@ -2,10 +2,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Mime;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -44,7 +46,7 @@ namespace Emby.Server.Implementations.Services
var pos = pathInfo.LastIndexOf('.'); var pos = pathInfo.LastIndexOf('.');
if (pos != -1) if (pos != -1)
{ {
var format = pathInfo.Substring(pos + 1); var format = pathInfo.AsSpan().Slice(pos + 1);
contentType = GetFormatContentType(format); contentType = GetFormatContentType(format);
if (contentType != null) if (contentType != null)
{ {
@ -55,15 +57,18 @@ namespace Emby.Server.Implementations.Services
return pathInfo; return pathInfo;
} }
private static string GetFormatContentType(string format) private static string GetFormatContentType(ReadOnlySpan<char> format)
{ {
// built-in formats if (format.Equals("json", StringComparison.Ordinal))
switch (format)
{ {
case "json": return "application/json"; return MediaTypeNames.Application.Json;
case "xml": return "application/xml";
default: return null;
} }
else if (format.Equals("xml", StringComparison.Ordinal))
{
return MediaTypeNames.Application.Xml;
}
return null;
} }
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken) public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
@ -78,7 +83,8 @@ namespace Emby.Server.Implementations.Services
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false); var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
httpHost.ApplyRequestFilters(httpReq, httpRes, request); httpHost.ApplyRequestFilters(httpReq, httpRes, request);
httpRes.HttpContext.SetServiceStackRequest(httpReq);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters // Apply response filters

View File

@ -124,7 +124,10 @@ namespace Emby.Server.Implementations.Services
var hasSeparators = new List<bool>(); var hasSeparators = new List<bool>();
foreach (var component in this.restPath.Split(PathSeperatorChar)) 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 if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
&& component.IndexOf(ComponentSeperator) != -1) && component.IndexOf(ComponentSeperator) != -1)
@ -153,7 +156,7 @@ namespace Emby.Server.Implementations.Services
{ {
var component = components[i]; var component = components[i];
if (component.StartsWith(VariablePrefix)) if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
{ {
var variableName = component.Substring(1, component.Length - 2); var variableName = component.Substring(1, component.Length - 2);
if (variableName[variableName.Length - 1] == WildCardChar) if (variableName[variableName.Length - 1] == WildCardChar)
@ -302,9 +305,9 @@ namespace Emby.Server.Implementations.Services
} }
// Routes with least wildcard matches get the highest score // 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 // 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 // Exact verb match is better than ANY
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase)) if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
@ -442,12 +445,14 @@ namespace Emby.Server.Implementations.Services
&& requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount; && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
if (!isValidWildCardPath) if (!isValidWildCardPath)
{
throw new ArgumentException( throw new ArgumentException(
string.Format( string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'", "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
pathInfo, pathInfo,
this.restPath)); this.restPath));
}
} }
var requestKeyValuesMap = new Dictionary<string, string>(); var requestKeyValuesMap = new Dictionary<string, string>();
@ -483,7 +488,8 @@ namespace Emby.Server.Implementations.Services
sb.Append(value); sb.Append(value);
for (var j = pathIx + 1; j < requestComponents.Length; j++) for (var j = pathIx + 1; j < requestComponents.Length; j++)
{ {
sb.Append(PathSeperatorChar + requestComponents[j]); sb.Append(PathSeperatorChar)
.Append(requestComponents[j]);
} }
value = sb.ToString(); value = sb.ToString();
@ -500,7 +506,8 @@ namespace Emby.Server.Implementations.Services
pathIx++; pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{ {
sb.Append(PathSeperatorChar + requestComponents[pathIx++]); sb.Append(PathSeperatorChar)
.Append(requestComponents[pathIx++]);
} }
value = sb.ToString(); value = sb.ToString();

View File

@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Session
} }
catch (DbUpdateConcurrencyException e) 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, Client = appName,
DeviceId = deviceId, DeviceId = deviceId,
ApplicationVersion = appVersion, ApplicationVersion = appVersion,
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture) Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
ServerId = _appHost.SystemId
}; };
var username = user?.Username; var username = user?.Username;

View File

@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem x, BaseItem y)
{ {
if (x == null) if (x == null)
{
throw new ArgumentNullException(nameof(x)); throw new ArgumentNullException(nameof(x));
}
if (y == null) if (y == null)
{
throw new ArgumentNullException(nameof(y)); throw new ArgumentNullException(nameof(y));
}
return DateTime.Compare(x.DateCreated, y.DateCreated); return DateTime.Compare(x.DateCreated, y.DateCreated);
} }

View File

@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem x, BaseItem y)
{ {
if (x == null) if (x == null)
{
throw new ArgumentNullException(nameof(x)); throw new ArgumentNullException(nameof(x));
}
if (y == null) if (y == null)
{
throw new ArgumentNullException(nameof(y)); throw new ArgumentNullException(nameof(y));
}
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0); return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
} }

View File

@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem x, BaseItem y)
{ {
if (x == null) if (x == null)
{
throw new ArgumentNullException(nameof(x)); throw new ArgumentNullException(nameof(x));
}
if (y == null) if (y == null)
{
throw new ArgumentNullException(nameof(y)); throw new ArgumentNullException(nameof(y));
}
return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase); return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
} }

View File

@ -194,26 +194,24 @@ namespace Emby.Server.Implementations.SyncPlay
} }
/// <inheritdoc /> /// <inheritdoc />
public void InitGroup(SessionInfo session, CancellationToken cancellationToken) public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
{ {
_group.AddSession(session); _group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this); _syncPlayManager.AddSessionToGroup(session, this);
_group.PlayingItem = session.FullNowPlayingItem; _group.PlayingItem = session.FullNowPlayingItem;
_group.IsPaused = true; _group.IsPaused = session.PlayState.IsPaused;
_group.PositionTicks = session.PlayState.PositionTicks ?? 0; _group.PositionTicks = session.PlayState.PositionTicks ?? 0;
_group.LastActivity = DateTime.UtcNow; _group.LastActivity = DateTime.UtcNow;
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) 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); _group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this); _syncPlayManager.AddSessionToGroup(session, this);
@ -224,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
// Client join and play, syncing will happen client side // Syncing will happen client-side
if (!_group.IsPaused) if (!_group.IsPaused)
{ {
var playCommand = NewSyncPlayCommand(SendCommandType.Play); var playCommand = NewSyncPlayCommand(SendCommandType.Play);
@ -262,10 +260,9 @@ namespace Emby.Server.Implementations.SyncPlay
/// <inheritdoc /> /// <inheritdoc />
public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
{ {
// The server's job is to mantain a consistent state to which clients refer to, // The server's job is to maintain a consistent state for clients to reference
// as also to notify clients of state changes. // and notify clients of state changes. The actual syncing of media playback
// The actual syncing of media playback happens client side. // happens client side. Clients are aware of the server's time and use it to sync.
// Clients are aware of the server's time and use it to sync.
switch (request.Type) switch (request.Type)
{ {
case PlaybackRequestType.Play: case PlaybackRequestType.Play:
@ -277,13 +274,13 @@ namespace Emby.Server.Implementations.SyncPlay
case PlaybackRequestType.Seek: case PlaybackRequestType.Seek:
HandleSeekRequest(session, request, cancellationToken); HandleSeekRequest(session, request, cancellationToken);
break; break;
case PlaybackRequestType.Buffering: case PlaybackRequestType.Buffer:
HandleBufferingRequest(session, request, cancellationToken); HandleBufferingRequest(session, request, cancellationToken);
break; break;
case PlaybackRequestType.BufferingDone: case PlaybackRequestType.Ready:
HandleBufferingDoneRequest(session, request, cancellationToken); HandleBufferingDoneRequest(session, request, cancellationToken);
break; break;
case PlaybackRequestType.UpdatePing: case PlaybackRequestType.Ping:
HandlePingUpdateRequest(session, request); HandlePingUpdateRequest(session, request);
break; break;
} }
@ -301,7 +298,7 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
// Pick a suitable time that accounts for latency // Pick a suitable time that accounts for latency
var delay = _group.GetHighestPing() * 2; 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 // Unpause group and set starting point in future
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) // 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 currentTime = DateTime.UtcNow;
var elapsedTime = currentTime - _group.LastActivity; var elapsedTime = currentTime - _group.LastActivity;
_group.LastActivity = currentTime; _group.LastActivity = currentTime;
// Seek only if playback actually started // 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; _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
var command = NewSyncPlayCommand(SendCommandType.Pause); 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 // Client, that was buffering, resumed playback but did not update others in time
delay = _group.GetHighestPing() * 2; delay = _group.GetHighestPing() * 2;
delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
_group.LastActivity = currentTime.AddMilliseconds( _group.LastActivity = currentTime.AddMilliseconds(
delay); delay);
@ -495,7 +493,7 @@ namespace Emby.Server.Implementations.SyncPlay
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
{ {
// Collected pings are used to account for network latency when unpausing playback // 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 /> /// <inheritdoc />

View File

@ -170,10 +170,11 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); _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 Type = GroupUpdateType.CreateGroupDenied
}; };
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return; return;
} }
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay
var group = new SyncPlayController(_sessionManager, this); var group = new SyncPlayController(_sessionManager, this);
_groups[group.GetGroupId()] = group; _groups[group.GetGroupId()] = group;
group.InitGroup(session, cancellationToken); group.CreateGroup(session, cancellationToken);
} }
} }
@ -205,6 +206,7 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
Type = GroupUpdateType.JoinGroupDenied Type = GroupUpdateType.JoinGroupDenied
}; };
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return; return;
} }
@ -300,9 +302,9 @@ namespace Emby.Server.Implementations.SyncPlay
group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select( group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
group => group.GetInfo()).ToList(); group => group.GetInfo()).ToList();
} }
// Otherwise show all available groups
else else
{ {
// Otherwise show all available groups
return _groups.Values.Where( return _groups.Values.Where(
group => HasAccessToItem(user, group.GetPlayingItemId())).Select( group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
group => group.GetInfo()).ToList(); group => group.GetInfo()).ToList();
@ -322,6 +324,7 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
Type = GroupUpdateType.JoinGroupDenied Type = GroupUpdateType.JoinGroupDenied
}; };
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return; return;
} }
@ -366,7 +369,6 @@ namespace Emby.Server.Implementations.SyncPlay
} }
_sessionToGroupMap.Remove(session.Id, out var tempGroup); _sessionToGroupMap.Remove(session.Id, out var tempGroup);
if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
{ {
throw new InvalidOperationException("Session was in wrong group!"); throw new InvalidOperationException("Session was in wrong group!");

View File

@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10; limit = limit.Value + 10;
} }
var items = _libraryManager.GetItemList(new InternalItemsQuery(user) var items = _libraryManager
{ .GetItemList(
IncludeItemTypes = new[] { typeof(Episode).Name }, new InternalItemsQuery(user)
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
SeriesPresentationUniqueKey = presentationUniqueKey,
Limit = limit,
DtoOptions = new DtoOptions
{
Fields = new ItemFields[]
{ {
ItemFields.SeriesPresentationUniqueKey IncludeItemTypes = new[] { typeof(Episode).Name },
}, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
EnableImages = false SeriesPresentationUniqueKey = presentationUniqueKey,
}, Limit = limit,
GroupBySeriesPresentationUniqueKey = true 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 // Avoid implicitly captured closure
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions); var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);

View File

@ -1,7 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -17,11 +16,10 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates; using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Updates namespace Emby.Server.Implementations.Updates
@ -31,11 +29,6 @@ namespace Emby.Server.Implementations.Updates
/// </summary> /// </summary>
public class InstallationManager : IInstallationManager 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> /// <summary>
/// The logger. /// The logger.
/// </summary> /// </summary>
@ -53,7 +46,6 @@ namespace Emby.Server.Implementations.Updates
private readonly IApplicationHost _applicationHost; private readonly IApplicationHost _applicationHost;
private readonly IZipClient _zipClient; private readonly IZipClient _zipClient;
private readonly IConfiguration _appConfig;
private readonly object _currentInstallationsLock = new object(); private readonly object _currentInstallationsLock = new object();
@ -75,8 +67,7 @@ namespace Emby.Server.Implementations.Updates
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IServerConfigurationManager config, IServerConfigurationManager config,
IFileSystem fileSystem, IFileSystem fileSystem,
IZipClient zipClient, IZipClient zipClient)
IConfiguration appConfig)
{ {
if (logger == null) if (logger == null)
{ {
@ -94,7 +85,6 @@ namespace Emby.Server.Implementations.Updates
_config = config; _config = config;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_zipClient = zipClient; _zipClient = zipClient;
_appConfig = appConfig;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -122,16 +112,14 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc /> /// <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 try
{ {
using (var response = await _httpClient.SendAsync( using (var response = await _httpClient.SendAsync(
new HttpRequestOptions new HttpRequestOptions
{ {
Url = manifestUrl, Url = manifest,
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
CacheMode = CacheMode.Unconditional, CacheMode = CacheMode.Unconditional,
CacheLength = TimeSpan.FromMinutes(3) CacheLength = TimeSpan.FromMinutes(3)
@ -145,23 +133,38 @@ namespace Emby.Server.Implementations.Updates
} }
catch (SerializationException ex) catch (SerializationException ex)
{ {
const string LogTemplate = _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
"Failed to deserialize the plugin manifest retrieved from {PluginManifestUrl}. If you " + return Array.Empty<PackageInfo>();
"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;
} }
} }
} }
catch (UriFormatException ex) catch (UriFormatException ex)
{ {
const string LogTemplate = _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
"The URL configured for the plugin repository manifest URL is not valid: {PluginManifestUrl}. " + return Array.Empty<PackageInfo>();
"Please check the URL configured by --plugin-manifest-url or " + PluginManifestUrlKey;
_logger.LogError(ex, LogTemplate, manifestUrl);
throw;
} }
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 /> /// <inheritdoc />
@ -402,6 +405,12 @@ namespace Emby.Server.Implementations.Updates
/// <param name="plugin">The plugin.</param> /// <param name="plugin">The plugin.</param>
public void UninstallPlugin(IPlugin plugin) public void UninstallPlugin(IPlugin plugin)
{ {
if (!plugin.CanUninstall)
{
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
return;
}
plugin.OnUninstalling(); plugin.OnUninstalling();
// Remove it the quick way for now // Remove it the quick way for now

View File

@ -39,21 +39,18 @@ namespace Jellyfin.Api.Auth
/// <inheritdoc /> /// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync() protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{ {
var authenticatedAttribute = new AuthenticatedAttribute();
try try
{ {
var user = _authService.Authenticate(Request, authenticatedAttribute); var authorizationInfo = _authService.Authenticate(Request);
if (user == null) if (authorizationInfo == null)
{ {
return Task.FromResult(AuthenticateResult.Fail("Invalid user")); return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
} }
var claims = new[] var claims = new[]
{ {
new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
new Claim( new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
ClaimTypes.Role,
value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
}; };
var identity = new ClaimsIdentity(claims, Scheme.Name); var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity); var principal = new ClaimsPrincipal(identity);

View File

@ -36,7 +36,6 @@ namespace Jellyfin.Api.Controllers
public void CompleteWizard() public void CompleteWizard()
{ {
_config.Configuration.IsStartupWizardCompleted = true; _config.Configuration.IsStartupWizardCompleted = true;
_config.SetOptimalValues();
_config.SaveConfiguration(); _config.SaveConfiguration();
} }
@ -47,14 +46,12 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Configuration")] [HttpGet("Configuration")]
public StartupConfigurationDto GetStartupConfiguration() public StartupConfigurationDto GetStartupConfiguration()
{ {
var result = new StartupConfigurationDto return new StartupConfigurationDto
{ {
UICulture = _config.Configuration.UICulture, UICulture = _config.Configuration.UICulture,
MetadataCountryCode = _config.Configuration.MetadataCountryCode, MetadataCountryCode = _config.Configuration.MetadataCountryCode,
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
}; };
return result;
} }
/// <summary> /// <summary>
@ -93,10 +90,10 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <returns>The first user.</returns> /// <returns>The first user.</returns>
[HttpGet("User")] [HttpGet("User")]
public StartupUserDto GetFirstUser() public async Task<StartupUserDto> GetFirstUser()
{ {
// TODO: Remove this method when startup wizard no longer requires an existing user. // 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(); var user = _userManager.Users.First();
return new StartupUserDto return new StartupUserDto
{ {

View File

@ -14,9 +14,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> <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="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>
<ItemGroup> <ItemGroup>

View File

@ -32,17 +32,28 @@ namespace Jellyfin.Data.Entities
/// <param name="_personrole1"></param> /// <param name="_personrole1"></param>
public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1) 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.Path = path;
this.Kind = kind; this.Kind = kind;
if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0)); if (_metadata0 == null)
{
throw new ArgumentNullException(nameof(_metadata0));
}
_metadata0.Artwork.Add(this); _metadata0.Artwork.Add(this);
if (_personrole1 == null) throw new ArgumentNullException(nameof(_personrole1)); if (_personrole1 == null)
_personrole1.Artwork = this; {
throw new ArgumentNullException(nameof(_personrole1));
}
_personrole1.Artwork = this;
Init(); Init();
} }
@ -87,7 +98,7 @@ namespace Jellyfin.Data.Entities
{ {
int value = _Id; int value = _Id;
GetId(ref value); GetId(ref value);
return (_Id = value); return _Id = value;
} }
protected set protected set
@ -126,7 +137,7 @@ namespace Jellyfin.Data.Entities
{ {
string value = _Path; string value = _Path;
GetPath(ref value); GetPath(ref value);
return (_Path = value); return _Path = value;
} }
set set
@ -163,7 +174,7 @@ namespace Jellyfin.Data.Entities
{ {
Enums.ArtKind value = _Kind; Enums.ArtKind value = _Kind;
GetKind(ref value); GetKind(ref value);
return (_Kind = value); return _Kind = value;
} }
set set

View File

@ -29,18 +29,30 @@ namespace Jellyfin.Data.Entities
/// <summary> /// <summary>
/// Public constructor with required data. /// Public constructor with required data.
/// </summary> /// </summary>
/// <param name="title">The title or name of the object</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="language">ISO-639-3 3-character language codes.</param>
/// <param name="_book0"></param> /// <param name="_book0"></param>
public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) 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; this.Title = title;
if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); if (string.IsNullOrEmpty(language))
{
throw new ArgumentNullException(nameof(language));
}
this.Language = language; this.Language = language;
if (_book0 == null) throw new ArgumentNullException(nameof(_book0)); if (_book0 == null)
{
throw new ArgumentNullException(nameof(_book0));
}
_book0.BookMetadata.Add(this); _book0.BookMetadata.Add(this);
this.Publishers = new HashSet<Company>(); this.Publishers = new HashSet<Company>();
@ -51,8 +63,8 @@ namespace Jellyfin.Data.Entities
/// <summary> /// <summary>
/// Static create function (for use in LINQ queries, etc.) /// Static create function (for use in LINQ queries, etc.)
/// </summary> /// </summary>
/// <param name="title">The title or name of the object</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="language">ISO-639-3 3-character language codes.</param>
/// <param name="_book0"></param> /// <param name="_book0"></param>
public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) 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; long? value = _ISBN;
GetISBN(ref value); GetISBN(ref value);
return (_ISBN = value); return _ISBN = value;
} }
set set

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