Merge branch 'master' into fix-resharper-warnings
# Conflicts: # Emby.Server.Implementations/Net/SocketFactory.cs # RSSDP/SsdpCommunicationsServer.cs # RSSDP/SsdpDeviceLocator.cs # RSSDP/SsdpDevicePublisher.cs
This commit is contained in:
commit
8d7e4229ca
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -27,11 +27,11 @@ jobs:
|
||||||
dotnet-version: '7.0.x'
|
dotnet-version: '7.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
|
uses: github/codeql-action/init@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
|
uses: github/codeql-action/autobuild@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
|
uses: github/codeql-action/analyze@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2
|
||||||
|
|
2
.github/workflows/repo-stale.yaml
vendored
2
.github/workflows/repo-stale.yaml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8
|
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
days-before-stale: 120
|
days-before-stale: 120
|
||||||
|
|
|
@ -25,15 +25,15 @@
|
||||||
<PackageVersion Include="libse" Version="3.6.13" />
|
<PackageVersion Include="libse" Version="3.6.13" />
|
||||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||||
|
@ -42,8 +42,8 @@
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||||
|
|
|
@ -228,7 +228,7 @@ namespace Emby.Dlna
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _fileSystem.GetFilePaths(path)
|
return _fileSystem.GetFilePaths(path)
|
||||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
|
||||||
.Select(i => ParseProfileFile(i, type))
|
.Select(i => ParseProfileFile(i, type))
|
||||||
.Where(i => i is not null)
|
.Where(i => i is not null)
|
||||||
.ToList()!; // We just filtered out all the nulls
|
.ToList()!; // We just filtered out all the nulls
|
||||||
|
|
|
@ -201,9 +201,10 @@ namespace Emby.Dlna.Main
|
||||||
{
|
{
|
||||||
if (_communicationsServer is null)
|
if (_communicationsServer is null)
|
||||||
{
|
{
|
||||||
var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
|
_communicationsServer = new SsdpCommunicationsServer(
|
||||||
|
_socketFactory,
|
||||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
_networkManager,
|
||||||
|
_logger)
|
||||||
{
|
{
|
||||||
IsShared = true
|
IsShared = true
|
||||||
};
|
};
|
||||||
|
@ -213,7 +214,7 @@ namespace Emby.Dlna.Main
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error starting ssdp handlers");
|
_logger.LogError(ex, "Error starting SSDP handlers");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,19 +26,18 @@ namespace Emby.Naming.Video
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
|
|
||||||
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = Path.GetFileNameWithoutExtension(path);
|
var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
|
||||||
var token = Path.GetExtension(path).TrimStart('.');
|
|
||||||
|
|
||||||
foreach (var rule in options.StubTypes)
|
foreach (var rule in options.StubTypes)
|
||||||
{
|
{
|
||||||
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
|
if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
stubType = rule.StubType;
|
stubType = rule.StubType;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -61,7 +61,7 @@ namespace Emby.Photos
|
||||||
item.SetImagePath(ImageType.Primary, item.Path);
|
item.SetImagePath(ImageType.Primary, item.Path);
|
||||||
|
|
||||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
|
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -101,7 +101,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||||
|
@ -133,7 +132,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <value>All concrete types.</value>
|
/// <value>All concrete types.</value>
|
||||||
private Type[] _allConcreteTypes;
|
private Type[] _allConcreteTypes;
|
||||||
|
|
||||||
private bool _disposed = false;
|
private bool _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||||
|
@ -184,26 +183,16 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
public bool CoreStartupHasCompleted { get; private set; }
|
public bool CoreStartupHasCompleted { get; private set; }
|
||||||
|
|
||||||
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
|
|
||||||
&& !_startupOptions.IsService
|
|
||||||
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public INetworkManager NetManager { get; private set; }
|
public INetworkManager NetManager { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
|
||||||
/// </summary>
|
|
||||||
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
|
|
||||||
public bool HasPendingRestart { get; private set; }
|
public bool HasPendingRestart { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsShuttingDown { get; private set; }
|
public bool ShouldRestart { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool ShouldRestart { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the logger.
|
/// Gets the logger.
|
||||||
|
@ -461,7 +450,7 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||||
|
|
||||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
|
||||||
|
|
||||||
// Initialize runtime stat collection
|
// Initialize runtime stat collection
|
||||||
if (ConfigurationManager.Configuration.EnableMetrics)
|
if (ConfigurationManager.Configuration.EnableMetrics)
|
||||||
|
@ -507,6 +496,8 @@ namespace Emby.Server.Implementations
|
||||||
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
||||||
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
||||||
|
|
||||||
|
serviceCollection.AddScoped<ISystemManager, SystemManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<TmdbClientManager>();
|
serviceCollection.AddSingleton<TmdbClientManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton(NetManager);
|
serviceCollection.AddSingleton(NetManager);
|
||||||
|
@ -850,24 +841,6 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Restart()
|
|
||||||
{
|
|
||||||
ShouldRestart = true;
|
|
||||||
Shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Shutdown()
|
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay(100).ConfigureAwait(false);
|
|
||||||
IsShuttingDown = true;
|
|
||||||
Resolve<IHostApplicationLifetime>().StopApplication();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the composable part assemblies.
|
/// Gets the composable part assemblies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -923,49 +896,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
|
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the system status.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Where this request originated.</param>
|
|
||||||
/// <returns>SystemInfo.</returns>
|
|
||||||
public SystemInfo GetSystemInfo(HttpRequest request)
|
|
||||||
{
|
|
||||||
return new SystemInfo
|
|
||||||
{
|
|
||||||
HasPendingRestart = HasPendingRestart,
|
|
||||||
IsShuttingDown = IsShuttingDown,
|
|
||||||
Version = ApplicationVersionString,
|
|
||||||
WebSocketPortNumber = HttpPort,
|
|
||||||
CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
|
|
||||||
Id = SystemId,
|
|
||||||
ProgramDataPath = ApplicationPaths.ProgramDataPath,
|
|
||||||
WebPath = ApplicationPaths.WebPath,
|
|
||||||
LogPath = ApplicationPaths.LogDirectoryPath,
|
|
||||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
|
||||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
|
||||||
CachePath = ApplicationPaths.CachePath,
|
|
||||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
|
||||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
|
||||||
ServerName = FriendlyName,
|
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
|
||||||
SupportsLibraryMonitor = true,
|
|
||||||
PackageName = _startupOptions.PackageName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
|
||||||
{
|
|
||||||
return new PublicSystemInfo
|
|
||||||
{
|
|
||||||
Version = ApplicationVersionString,
|
|
||||||
ProductName = ApplicationProductName,
|
|
||||||
Id = SystemId,
|
|
||||||
ServerName = FriendlyName,
|
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
|
||||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(IPAddress remoteAddr)
|
public string GetSmartApiUrl(IPAddress remoteAddr)
|
||||||
{
|
{
|
||||||
|
@ -983,7 +913,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(HttpRequest request)
|
public string GetSmartApiUrl(HttpRequest request)
|
||||||
{
|
{
|
||||||
// Return the host in the HTTP request as the API url
|
// Return the host in the HTTP request as the API URL if not configured otherwise
|
||||||
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
|
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
|
||||||
{
|
{
|
||||||
int? requestPort = request.Host.Port;
|
int? requestPort = request.Host.Port;
|
||||||
|
@ -1018,7 +948,7 @@ namespace Emby.Server.Implementations
|
||||||
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
|
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
|
||||||
{
|
{
|
||||||
// With an empty source, the port will be null
|
// With an empty source, the port will be null
|
||||||
var smart = NetManager.GetBindAddress(ipAddress, out _, true);
|
var smart = NetManager.GetBindAddress(ipAddress, out _, false);
|
||||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||||
int? port = !allowHttps ? HttpPort : null;
|
int? port = !allowHttps ? HttpPort : null;
|
||||||
return GetLocalApiUrl(smart, scheme, port);
|
return GetLocalApiUrl(smart, scheme, port);
|
||||||
|
|
|
@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
|
||||||
namespace Emby.Server.Implementations.EntryPoints
|
namespace Emby.Server.Implementations.EntryPoints
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class UdpServerEntryPoint.
|
/// Class responsible for registering all UDP broadcast endpoints and their handlers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UdpServerEntryPoint : IServerEntryPoint
|
public sealed class UdpServerEntryPoint : IServerEntryPoint
|
||||||
{
|
{
|
||||||
|
@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private readonly IConfigurationManager _configurationManager;
|
private readonly IConfigurationManager _configurationManager;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly bool _enableMultiSocketBinding;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The UDP server.
|
/// The UDP server.
|
||||||
|
@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
_udpServers = new List<UdpServer>();
|
_udpServers = new List<UdpServer>();
|
||||||
_enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_enableMultiSocketBinding)
|
// Linux needs to bind to the broadcast addresses to get broadcast traffic
|
||||||
|
// Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
{
|
{
|
||||||
// Add global broadcast socket
|
// Add global broadcast listener
|
||||||
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
|
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
|
||||||
server.Start(_cancellationTokenSource.Token);
|
server.Start(_cancellationTokenSource.Token);
|
||||||
_udpServers.Add(server);
|
_udpServers.Add(server);
|
||||||
|
|
||||||
// Add bind address specific broadcast sockets
|
// Add bind address specific broadcast listeners
|
||||||
// IPv6 is currently unsupported
|
// IPv6 is currently unsupported
|
||||||
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
|
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
|
||||||
foreach (var intf in validInterfaces)
|
foreach (var intf in validInterfaces)
|
||||||
|
@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber);
|
// Add bind address specific broadcast listeners
|
||||||
server.Start(_cancellationTokenSource.Token);
|
// IPv6 is currently unsupported
|
||||||
_udpServers.Add(server);
|
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
|
||||||
|
foreach (var intf in validInterfaces)
|
||||||
|
{
|
||||||
|
var intfAddress = intf.Address;
|
||||||
|
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
|
||||||
|
|
||||||
|
var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
|
||||||
|
server.Start(_cancellationTokenSource.Token);
|
||||||
|
_udpServers.Add(server);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (SocketException ex)
|
catch (SocketException ex)
|
||||||
|
@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
throw new ObjectDisposedException(this.GetType().Name);
|
throw new ObjectDisposedException(GetType().Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var filePathSpan = filePath.AsSpan();
|
||||||
|
|
||||||
// relative path
|
// relative path
|
||||||
if (firstChar == '\\')
|
if (firstChar == '\\')
|
||||||
{
|
{
|
||||||
filePath = filePath.Substring(1);
|
filePathSpan = filePathSpan.Slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Path.GetFullPath(Path.Combine(folderPath, filePath));
|
return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
|
|
@ -838,19 +838,12 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
var path = Person.GetPath(name);
|
var path = Person.GetPath(name);
|
||||||
var id = GetItemByNameId<Person>(path);
|
var id = GetItemByNameId<Person>(path);
|
||||||
if (GetItemById(id) is not Person item)
|
if (GetItemById(id) is Person item)
|
||||||
{
|
{
|
||||||
item = new Person
|
return item;
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Id = id,
|
|
||||||
DateCreated = DateTime.UtcNow,
|
|
||||||
DateModified = DateTime.UtcNow,
|
|
||||||
Path = path
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1161,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
Name = Path.GetFileName(dir),
|
Name = Path.GetFileName(dir),
|
||||||
|
|
||||||
Locations = _fileSystem.GetFilePaths(dir, false)
|
Locations = _fileSystem.GetFilePaths(dir, false)
|
||||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -2899,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
|
||||||
var saveEntity = false;
|
var saveEntity = false;
|
||||||
var personEntity = GetPerson(person.Name);
|
var personEntity = GetPerson(person.Name);
|
||||||
|
|
||||||
// if PresentationUniqueKey is empty it's likely a new item.
|
if (personEntity is null)
|
||||||
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
|
|
||||||
{
|
{
|
||||||
|
var path = Person.GetPath(person.Name);
|
||||||
|
personEntity = new Person()
|
||||||
|
{
|
||||||
|
Name = person.Name,
|
||||||
|
Id = GetItemByNameId<Person>(path),
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow,
|
||||||
|
Path = path
|
||||||
|
};
|
||||||
|
|
||||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||||
saveEntity = true;
|
saveEntity = true;
|
||||||
}
|
}
|
||||||
|
@ -3134,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
|
|
||||||
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
||||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(shortcut))
|
if (!string.IsNullOrEmpty(shortcut))
|
||||||
|
|
|
@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||||
|
|
||||||
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(args.Path);
|
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
|
if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// if audio file exists of same name, return null
|
// if audio file exists of same name, return null
|
||||||
return null;
|
return null;
|
||||||
|
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||||
|
|
||||||
if (item is not null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
|
item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
item.IsInMixedFolder = true;
|
item.IsInMixedFolder = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
|
return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||||
return GetBook(args);
|
return GetBook(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(args.Path);
|
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||||
|
|
||||||
if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's a book
|
// It's a book
|
||||||
return new Book
|
return new Book
|
||||||
|
@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||||
{
|
{
|
||||||
var bookFiles = args.FileSystemChildren.Where(f =>
|
var bookFiles = args.FileSystemChildren.Where(f =>
|
||||||
{
|
{
|
||||||
var fileExtension = Path.GetExtension(f.FullName)
|
var fileExtension = Path.GetExtension(f.FullName.AsSpan());
|
||||||
?? string.Empty;
|
|
||||||
|
|
||||||
return _validExtensions.Contains(
|
return _validExtensions.Contains(
|
||||||
fileExtension,
|
fileExtension,
|
||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
@ -121,5 +121,7 @@
|
||||||
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
||||||
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
||||||
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
||||||
"External": "പുറമേയുള്ള"
|
"External": "പുറമേയുള്ള",
|
||||||
|
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
|
||||||
|
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,5 +124,5 @@
|
||||||
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
|
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
|
||||||
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
|
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
|
||||||
"External": "Externé",
|
"External": "Externé",
|
||||||
"HearingImpaired": "Sluchovo Postihnutý"
|
"HearingImpaired": "Sluchovo postihnutí"
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
|
||||||
{
|
{
|
||||||
var deadImages = images
|
var deadImages = images
|
||||||
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
|
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var image in deadImages)
|
foreach (var image in deadImages)
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Net
|
namespace Emby.Server.Implementations.Net
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Factory class to create different kinds of sockets.
|
||||||
|
/// </summary>
|
||||||
public class SocketFactory : ISocketFactory
|
public class SocketFactory : ISocketFactory
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
|
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(bindInterface.Address);
|
var interfaceAddress = bindInterface.Address;
|
||||||
|
ArgumentNullException.ThrowIfNull(interfaceAddress);
|
||||||
|
|
||||||
if (localPort < 0)
|
if (localPort < 0)
|
||||||
{
|
{
|
||||||
|
@ -49,7 +53,7 @@ namespace Emby.Server.Implementations.Net
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||||
socket.Bind(new IPEndPoint(bindInterface.Address, localPort));
|
socket.Bind(new IPEndPoint(interfaceAddress, localPort));
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
@ -82,16 +86,25 @@ namespace Emby.Server.Implementations.Net
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var interfaceIndex = bindInterface.Index;
|
|
||||||
var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
|
|
||||||
|
|
||||||
socket.MulticastLoopback = false;
|
socket.MulticastLoopback = false;
|
||||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
|
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
|
||||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
|
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
|
||||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
|
|
||||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
socket.Bind(new IPEndPoint(multicastAddress, localPort));
|
{
|
||||||
|
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
|
||||||
|
socket.Bind(new IPEndPoint(multicastAddress, localPort));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Only create socket if interface supports multicast
|
||||||
|
var interfaceIndex = bindInterface.Index;
|
||||||
|
var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
|
||||||
|
|
||||||
|
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
|
||||||
|
socket.Bind(new IPEndPoint(bindIPAddress, localPort));
|
||||||
|
}
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
|
@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
// this is probably best done as a metadata provider
|
// this is probably best done as a metadata provider
|
||||||
// saving a file over itself will require some work to prevent this from happening when not needed
|
// saving a file over itself will require some work to prevent this from happening when not needed
|
||||||
var playlistPath = item.Path;
|
var playlistPath = item.Path;
|
||||||
var extension = Path.GetExtension(playlistPath);
|
var extension = Path.GetExtension(playlistPath.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var playlist = new WplPlaylist();
|
var playlist = new WplPlaylist();
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
|
@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
string text = new WplContent().ToText(playlist);
|
string text = new WplContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new ZplPlaylist();
|
var playlist = new ZplPlaylist();
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
|
@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
string text = new ZplContent().ToText(playlist);
|
string text = new ZplContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new M3uPlaylist
|
var playlist = new M3uPlaylist
|
||||||
{
|
{
|
||||||
|
@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
string text = new M3uContent().ToText(playlist);
|
string text = new M3uContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new M3uPlaylist();
|
var playlist = new M3uPlaylist();
|
||||||
playlist.IsExtended = true;
|
playlist.IsExtended = true;
|
||||||
|
@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
string text = new M3uContent().ToText(playlist);
|
string text = new M3uContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new PlsPlaylist();
|
var playlist = new PlsPlaylist();
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
|
|
103
Emby.Server.Implementations/SystemManager.cs
Normal file
103
Emby.Server.Implementations/SystemManager.cs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Updates;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Model.System;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class SystemManager : ISystemManager
|
||||||
|
{
|
||||||
|
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||||
|
private readonly IServerApplicationHost _applicationHost;
|
||||||
|
private readonly IServerApplicationPaths _applicationPaths;
|
||||||
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
private readonly IStartupOptions _startupOptions;
|
||||||
|
private readonly IInstallationManager _installationManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SystemManager"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param>
|
||||||
|
/// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param>
|
||||||
|
/// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param>
|
||||||
|
/// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
|
||||||
|
/// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
|
||||||
|
/// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
|
||||||
|
public SystemManager(
|
||||||
|
IHostApplicationLifetime applicationLifetime,
|
||||||
|
IServerApplicationHost applicationHost,
|
||||||
|
IServerApplicationPaths applicationPaths,
|
||||||
|
IServerConfigurationManager configurationManager,
|
||||||
|
IStartupOptions startupOptions,
|
||||||
|
IInstallationManager installationManager)
|
||||||
|
{
|
||||||
|
_applicationLifetime = applicationLifetime;
|
||||||
|
_applicationHost = applicationHost;
|
||||||
|
_applicationPaths = applicationPaths;
|
||||||
|
_configurationManager = configurationManager;
|
||||||
|
_startupOptions = startupOptions;
|
||||||
|
_installationManager = installationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public SystemInfo GetSystemInfo(HttpRequest request)
|
||||||
|
{
|
||||||
|
return new SystemInfo
|
||||||
|
{
|
||||||
|
HasPendingRestart = _applicationHost.HasPendingRestart,
|
||||||
|
IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
|
||||||
|
Version = _applicationHost.ApplicationVersionString,
|
||||||
|
WebSocketPortNumber = _applicationHost.HttpPort,
|
||||||
|
CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
|
||||||
|
Id = _applicationHost.SystemId,
|
||||||
|
ProgramDataPath = _applicationPaths.ProgramDataPath,
|
||||||
|
WebPath = _applicationPaths.WebPath,
|
||||||
|
LogPath = _applicationPaths.LogDirectoryPath,
|
||||||
|
ItemsByNamePath = _applicationPaths.InternalMetadataPath,
|
||||||
|
InternalMetadataPath = _applicationPaths.InternalMetadataPath,
|
||||||
|
CachePath = _applicationPaths.CachePath,
|
||||||
|
TranscodingTempPath = _configurationManager.GetTranscodePath(),
|
||||||
|
ServerName = _applicationHost.FriendlyName,
|
||||||
|
LocalAddress = _applicationHost.GetSmartApiUrl(request),
|
||||||
|
SupportsLibraryMonitor = true,
|
||||||
|
PackageName = _startupOptions.PackageName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
||||||
|
{
|
||||||
|
return new PublicSystemInfo
|
||||||
|
{
|
||||||
|
Version = _applicationHost.ApplicationVersionString,
|
||||||
|
ProductName = _applicationHost.Name,
|
||||||
|
Id = _applicationHost.SystemId,
|
||||||
|
ServerName = _applicationHost.FriendlyName,
|
||||||
|
LocalAddress = _applicationHost.GetSmartApiUrl(request),
|
||||||
|
StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Restart() => ShutdownInternal(true);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Shutdown() => ShutdownInternal(false);
|
||||||
|
|
||||||
|
private void ShutdownInternal(bool restart)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(100).ConfigureAwait(false);
|
||||||
|
_applicationHost.ShouldRestart = restart;
|
||||||
|
_applicationLifetime.StopApplication();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
|
||||||
|
|
||||||
_endpoint = new IPEndPoint(bindAddress, port);
|
_endpoint = new IPEndPoint(bindAddress, port);
|
||||||
|
|
||||||
_udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
_udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
|
||||||
|
{
|
||||||
|
MulticastLoopback = false,
|
||||||
|
};
|
||||||
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Sending AutoDiscovery response");
|
||||||
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
|
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (SocketException ex)
|
catch (SocketException ex)
|
||||||
|
@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false);
|
var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
|
||||||
|
var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
|
||||||
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
|
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
|
||||||
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
|
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Don't throw
|
_logger.LogDebug("Broadcast socket operation cancelled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
|
|
||||||
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(package.SourceUrl);
|
if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
|
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||||
private const string DefaultEventEncoderPreset = "superfast";
|
private const string DefaultEventEncoderPreset = "superfast";
|
||||||
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
||||||
|
|
||||||
|
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
|
||||||
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IDlnaManager _dlnaManager;
|
private readonly IDlnaManager _dlnaManager;
|
||||||
|
@ -1705,16 +1707,31 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||||
|
|
||||||
|
// opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
|
||||||
|
var strictArgs = string.Empty;
|
||||||
|
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
||||||
|
if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
|
||||||
|
{
|
||||||
|
strictArgs = " -strict -2";
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.IsOutputVideo)
|
if (!state.IsOutputVideo)
|
||||||
{
|
{
|
||||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
|
||||||
{
|
|
||||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioTranscodeParams = string.Empty;
|
var audioTranscodeParams = string.Empty;
|
||||||
|
|
||||||
audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs;
|
// -vn to drop any video streams
|
||||||
|
audioTranscodeParams += "-vn";
|
||||||
|
|
||||||
|
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||||
|
{
|
||||||
|
return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
|
||||||
|
|
||||||
var audioBitrate = state.OutputAudioBitrate;
|
var audioBitrate = state.OutputAudioBitrate;
|
||||||
var audioChannels = state.OutputAudioChannels;
|
var audioChannels = state.OutputAudioChannels;
|
||||||
|
@ -1742,21 +1759,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
audioTranscodeParams += " -vn";
|
|
||||||
return audioTranscodeParams;
|
return audioTranscodeParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// dts, flac, opus and truehd are experimental in mp4 muxer
|
|
||||||
var strictArgs = string.Empty;
|
|
||||||
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
|
||||||
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
strictArgs = " -strict -2";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||||
{
|
{
|
||||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||||
|
@ -2041,9 +2046,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
|
||||||
|
|
||||||
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
|
var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
|
||||||
|
|
||||||
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||||
{
|
{
|
||||||
// TODO: Deprecate with new iOS app
|
// TODO: Deprecate with new iOS app
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
|
@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||||
{
|
{
|
||||||
var file = playlistId + Path.GetExtension(Request.Path);
|
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|
||||||
|
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid segment.");
|
return BadRequest("Invalid segment.");
|
||||||
}
|
}
|
||||||
|
@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||||
[FromRoute, Required] string segmentId,
|
[FromRoute, Required] string segmentId,
|
||||||
[FromRoute, Required] string segmentContainer)
|
[FromRoute, Required] string segmentContainer)
|
||||||
{
|
{
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
|
||||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||||
|
|
|
@ -7,6 +7,7 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Attributes;
|
using Jellyfin.Api.Attributes;
|
||||||
|
@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Stream GetFromBase64Stream(Stream inputStream)
|
||||||
|
=> new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the user image.
|
/// Sets the user image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
|
||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
|
@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
|
||||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||||
|
|
||||||
await _providerManager
|
await _providerManager
|
||||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
|
||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
|
@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
|
||||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||||
|
|
||||||
await _providerManager
|
await _providerManager
|
||||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
|
||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
|
||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
|
||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
||||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
|
@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
|
||||||
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
await using (fs.ConfigureAwait(false))
|
await using (fs.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(inputStream);
|
|
||||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
var bytes = Convert.FromBase64String(text);
|
|
||||||
return new MemoryStream(bytes, 0, bytes.Length, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
|
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
|
||||||
{
|
{
|
||||||
int? width = null;
|
int? width = null;
|
||||||
|
|
|
@ -6,6 +6,7 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
|
||||||
[FromBody, Required] UploadSubtitleDto body)
|
[FromBody, Required] UploadSubtitleDto body)
|
||||||
{
|
{
|
||||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||||
var data = Convert.FromBase64String(body.Data);
|
var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||||
var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
|
await using (stream.ConfigureAwait(false))
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
await _subtitleManager.UploadSubtitle(
|
await _subtitleManager.UploadSubtitle(
|
||||||
video,
|
video,
|
||||||
|
@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
|
||||||
Language = body.Language,
|
Language = body.Language,
|
||||||
IsForced = body.IsForced,
|
IsForced = body.IsForced,
|
||||||
IsHearingImpaired = body.IsHearingImpaired,
|
IsHearingImpaired = body.IsHearingImpaired,
|
||||||
Stream = memoryStream
|
Stream = stream
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.System;
|
using MediaBrowser.Model.System;
|
||||||
|
@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SystemController : BaseJellyfinApiController
|
public class SystemController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<SystemController> _logger;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly INetworkManager _network;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly ILogger<SystemController> _logger;
|
private readonly ISystemManager _systemManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
||||||
|
/// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
|
||||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||||
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
|
/// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
/// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
|
||||||
public SystemController(
|
public SystemController(
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
ILogger<SystemController> logger,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
|
IServerApplicationPaths appPaths,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
INetworkManager network,
|
INetworkManager networkManager,
|
||||||
ILogger<SystemController> logger)
|
ISystemManager systemManager)
|
||||||
{
|
{
|
||||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
|
||||||
_appHost = appHost;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_network = network;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_appHost = appHost;
|
||||||
|
_appPaths = appPaths;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_systemManager = systemManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult<SystemInfo> GetSystemInfo()
|
public ActionResult<SystemInfo> GetSystemInfo()
|
||||||
{
|
=> _systemManager.GetSystemInfo(Request);
|
||||||
return _appHost.GetSystemInfo(Request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets public information about the server.
|
/// Gets public information about the server.
|
||||||
|
@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
|
||||||
[HttpGet("Info/Public")]
|
[HttpGet("Info/Public")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||||
{
|
=> _systemManager.GetPublicSystemInfo(Request);
|
||||||
return _appHost.GetPublicSystemInfo(Request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pings the system.
|
/// Pings the system.
|
||||||
|
@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
|
||||||
[HttpPost("Ping", Name = "PostPingSystem")]
|
[HttpPost("Ping", Name = "PostPingSystem")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<string> PingSystem()
|
public ActionResult<string> PingSystem()
|
||||||
{
|
=> _appHost.Name;
|
||||||
return _appHost.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Restarts the application.
|
/// Restarts the application.
|
||||||
|
@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult RestartApplication()
|
public ActionResult RestartApplication()
|
||||||
{
|
{
|
||||||
_appHost.Restart();
|
_systemManager.Restart();
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult ShutdownApplication()
|
public ActionResult ShutdownApplication()
|
||||||
{
|
{
|
||||||
_appHost.Shutdown();
|
_systemManager.Shutdown();
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
|
||||||
return new EndPointInfo
|
return new EndPointInfo
|
||||||
{
|
{
|
||||||
IsLocal = HttpContext.IsLocal(),
|
IsLocal = HttpContext.IsLocal(),
|
||||||
IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
||||||
{
|
{
|
||||||
var result = _network.GetMacAddresses()
|
var result = _networkManager.GetMacAddresses()
|
||||||
.Select(i => new WakeOnLanInfo(i));
|
.Select(i => new WakeOnLanInfo(i));
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,13 +200,6 @@ public class DynamicHlsHelper
|
||||||
|
|
||||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||||
{
|
{
|
||||||
// Provide a workaround for the case issue between flac and fLaC.
|
|
||||||
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
|
|
||||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
|
||||||
{
|
|
||||||
builder.Append(flacWaPlaylist);
|
|
||||||
}
|
|
||||||
|
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||||
|
|
||||||
// Provide SDR HEVC entrance for backward compatibility.
|
// Provide SDR HEVC entrance for backward compatibility.
|
||||||
|
@ -236,14 +229,7 @@ public class DynamicHlsHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||||
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||||
|
|
||||||
// Provide a workaround for the case issue between flac and fLaC.
|
|
||||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
|
|
||||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
|
||||||
{
|
|
||||||
builder.Append(flacWaPlaylist);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore the video codec
|
// Restore the video codec
|
||||||
state.OutputVideoCodec = "copy";
|
state.OutputVideoCodec = "copy";
|
||||||
|
@ -274,13 +260,6 @@ public class DynamicHlsHelper
|
||||||
state.VideoStream.Level = originalLevel;
|
state.VideoStream.Level = originalLevel;
|
||||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||||
builder.Append(newPlaylist);
|
builder.Append(newPlaylist);
|
||||||
|
|
||||||
// Provide a workaround for the case issue between flac and fLaC.
|
|
||||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
|
|
||||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
|
||||||
{
|
|
||||||
builder.Append(flacWaPlaylist);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -767,16 +746,4 @@ public class DynamicHlsHelper
|
||||||
newValue.ToString(),
|
newValue.ToString(),
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
|
|
||||||
{
|
|
||||||
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@ using System.Text;
|
||||||
namespace Jellyfin.Api.Helpers;
|
namespace Jellyfin.Api.Helpers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hls Codec string helpers.
|
/// Helpers to generate HLS codec strings according to
|
||||||
|
/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
|
||||||
|
/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HlsCodecStringHelpers
|
public static class HlsCodecStringHelpers
|
||||||
{
|
{
|
||||||
|
@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for FLAC.
|
/// Codec name for FLAC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FLAC = "flac";
|
public const string FLAC = "fLaC";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for ALAC.
|
/// Codec name for ALAC.
|
||||||
|
@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for OPUS.
|
/// Codec name for OPUS.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string OPUS = "opus";
|
public const string OPUS = "Opus";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a MP3 codec string.
|
/// Gets a MP3 codec string.
|
||||||
|
|
|
@ -248,7 +248,7 @@ public static class StreamingHelpers
|
||||||
? GetOutputFileExtension(state, mediaSource)
|
? GetOutputFileExtension(state, mediaSource)
|
||||||
: ("." + state.OutputContainer);
|
: ("." + state.OutputContainer);
|
||||||
|
|
||||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -421,10 +421,9 @@ public static class StreamingHelpers
|
||||||
/// <param name="state">The state.</param>
|
/// <param name="state">The state.</param>
|
||||||
/// <param name="mediaSource">The mediaSource.</param>
|
/// <param name="mediaSource">The mediaSource.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(state.RequestedUrl);
|
var ext = Path.GetExtension(state.RequestedUrl);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ext))
|
if (!string.IsNullOrEmpty(ext))
|
||||||
{
|
{
|
||||||
return ext;
|
return ext;
|
||||||
|
@ -463,10 +462,9 @@ public static class StreamingHelpers
|
||||||
return ".asf";
|
return ".asf";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// Try to infer based on the desired audio codec
|
|
||||||
if (!state.IsVideoRequest)
|
|
||||||
{
|
{
|
||||||
|
// Try to infer based on the desired audio codec
|
||||||
var audioCodec = state.Request.AudioCodec;
|
var audioCodec = state.Request.AudioCodec;
|
||||||
|
|
||||||
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -497,7 +495,7 @@ public static class StreamingHelpers
|
||||||
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
|
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
throw new InvalidOperationException("Failed to find an appropriate file extension");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -514,7 +512,7 @@ public static class StreamingHelpers
|
||||||
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
||||||
|
|
||||||
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
var ext = outputFileExtension?.ToLowerInvariant();
|
var ext = outputFileExtension.ToLowerInvariant();
|
||||||
var folder = serverConfigurationManager.GetTranscodePath();
|
var folder = serverConfigurationManager.GetTranscodePath();
|
||||||
|
|
||||||
return Path.Combine(folder, filename + ext);
|
return Path.Combine(folder, filename + ext);
|
||||||
|
|
|
@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
|
||||||
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
|
if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
string subtitlePath = state.SubtitleStream.Path;
|
string subtitlePath = state.SubtitleStream.Path;
|
||||||
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
|
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
|
||||||
|
|
|
@ -230,12 +230,12 @@ public static partial class NetworkExtensions
|
||||||
}
|
}
|
||||||
else if (address.AddressFamily == AddressFamily.InterNetwork)
|
else if (address.AddressFamily == AddressFamily.InterNetwork)
|
||||||
{
|
{
|
||||||
result = new IPNetwork(address, Network.MinimumIPv4PrefixSize);
|
result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
{
|
{
|
||||||
result = new IPNetwork(address, Network.MinimumIPv6PrefixSize);
|
result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -283,12 +283,15 @@ public static partial class NetworkExtensions
|
||||||
|
|
||||||
if (hosts.Count <= 2)
|
if (hosts.Count <= 2)
|
||||||
{
|
{
|
||||||
|
var firstPart = hosts[0];
|
||||||
|
|
||||||
// Is hostname or hostname:port
|
// Is hostname or hostname:port
|
||||||
if (FqdnGeneratedRegex().IsMatch(hosts[0]))
|
if (FqdnGeneratedRegex().IsMatch(firstPart))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
addresses = Dns.GetHostAddresses(hosts[0]);
|
// .NET automatically filters only supported returned addresses based on OS support.
|
||||||
|
addresses = Dns.GetHostAddresses(firstPart);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (SocketException)
|
catch (SocketException)
|
||||||
|
@ -298,7 +301,7 @@ public static partial class NetworkExtensions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is an IPv4 or IPv4:port
|
// Is an IPv4 or IPv4:port
|
||||||
if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address))
|
if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
|
||||||
{
|
{
|
||||||
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|
||||||
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
|
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
|
||||||
|
|
|
@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||||
|
|
||||||
namespace Jellyfin.Networking.Manager
|
namespace Jellyfin.Networking.Manager
|
||||||
{
|
{
|
||||||
|
@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
|
||||||
|
|
||||||
private readonly IConfigurationManager _configurationManager;
|
private readonly IConfigurationManager _configurationManager;
|
||||||
|
|
||||||
|
private readonly IConfiguration _startupConfig;
|
||||||
|
|
||||||
private readonly object _networkEventLock;
|
private readonly object _networkEventLock;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Holds the published server URLs and the IPs to use them on.
|
/// Holds the published server URLs and the IPs to use them on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IReadOnlyDictionary<IPData, string> _publishedServerUrls;
|
private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
|
||||||
|
|
||||||
private IReadOnlyList<IPNetwork> _remoteAddressFilter;
|
private IReadOnlyList<IPNetwork> _remoteAddressFilter;
|
||||||
|
|
||||||
|
@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="NetworkManager"/> class.
|
/// Initializes a new instance of the <see cref="NetworkManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configurationManager">IServerConfigurationManager instance.</param>
|
/// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param>
|
||||||
|
/// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param>
|
||||||
/// <param name="logger">Logger to use for messages.</param>
|
/// <param name="logger">Logger to use for messages.</param>
|
||||||
#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
|
#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
|
||||||
public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger)
|
public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
ArgumentNullException.ThrowIfNull(configurationManager);
|
ArgumentNullException.ThrowIfNull(configurationManager);
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
_initLock = new();
|
_initLock = new();
|
||||||
_interfaces = new List<IPData>();
|
_interfaces = new List<IPData>();
|
||||||
_macAddresses = new List<PhysicalAddress>();
|
_macAddresses = new List<PhysicalAddress>();
|
||||||
_publishedServerUrls = new Dictionary<IPData, string>();
|
_publishedServerUrls = new List<PublishedServerUriOverride>();
|
||||||
_networkEventLock = new object();
|
_networkEventLock = new object();
|
||||||
_remoteAddressFilter = new List<IPNetwork>();
|
_remoteAddressFilter = new List<IPNetwork>();
|
||||||
|
|
||||||
|
@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Published server override list.
|
/// Gets the Published server override list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls;
|
public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
|
||||||
{
|
{
|
||||||
if (!_eventfire)
|
if (!_eventfire)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Network Address Change Event.");
|
|
||||||
// As network events tend to fire one after the other only fire once every second.
|
// As network events tend to fire one after the other only fire once every second.
|
||||||
_eventfire = true;
|
_eventfire = true;
|
||||||
OnNetworkChange();
|
OnNetworkChange();
|
||||||
|
@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InitialiseInterfaces();
|
InitializeInterfaces();
|
||||||
InitialiseLan(networkConfig);
|
InitializeLan(networkConfig);
|
||||||
EnforceBindSettings(networkConfig);
|
EnforceBindSettings(networkConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PrintNetworkInformation(networkConfig);
|
||||||
NetworkChanged?.Invoke(this, EventArgs.Empty);
|
NetworkChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager
|
||||||
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
|
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
|
||||||
/// Generate a list of all active mac addresses that aren't loopback addresses.
|
/// Generate a list of all active mac addresses that aren't loopback addresses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitialiseInterfaces()
|
private void InitializeInterfaces()
|
||||||
{
|
{
|
||||||
lock (_initLock)
|
lock (_initLock)
|
||||||
{
|
{
|
||||||
|
@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var nics = NetworkInterface.GetAllNetworkInterfaces()
|
var nics = NetworkInterface.GetAllNetworkInterfaces()
|
||||||
.Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
|
.Where(i => i.OperationalStatus == OperationalStatus.Up);
|
||||||
|
|
||||||
foreach (NetworkInterface adapter in nics)
|
foreach (NetworkInterface adapter in nics)
|
||||||
{
|
{
|
||||||
|
@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
|
||||||
{
|
{
|
||||||
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
|
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||||
{
|
{
|
||||||
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
|
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
|
||||||
interfaceObject.Index = ipProperties.GetIPv4Properties().Index;
|
{
|
||||||
interfaceObject.Name = adapter.Name;
|
Index = ipProperties.GetIPv4Properties().Index,
|
||||||
|
Name = adapter.Name,
|
||||||
|
SupportsMulticast = adapter.SupportsMulticast
|
||||||
|
};
|
||||||
|
|
||||||
interfaces.Add(interfaceObject);
|
interfaces.Add(interfaceObject);
|
||||||
}
|
}
|
||||||
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
|
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
{
|
{
|
||||||
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
|
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
|
||||||
interfaceObject.Index = ipProperties.GetIPv6Properties().Index;
|
{
|
||||||
interfaceObject.Name = adapter.Name;
|
Index = ipProperties.GetIPv6Properties().Index,
|
||||||
|
Name = adapter.Name,
|
||||||
|
SupportsMulticast = adapter.SupportsMulticast
|
||||||
|
};
|
||||||
|
|
||||||
interfaces.Add(interfaceObject);
|
interfaces.Add(interfaceObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning disable CA1031 // Do not catch general exception types
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
#pragma warning restore CA1031 // Do not catch general exception types
|
|
||||||
{
|
{
|
||||||
// Ignore error, and attempt to continue.
|
// Ignore error, and attempt to continue.
|
||||||
_logger.LogError(ex, "Error encountered parsing interfaces.");
|
_logger.LogError(ex, "Error encountered parsing interfaces.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning disable CA1031 // Do not catch general exception types
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
#pragma warning restore CA1031 // Do not catch general exception types
|
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error obtaining interfaces.");
|
_logger.LogError(ex, "Error obtaining interfaces.");
|
||||||
}
|
}
|
||||||
|
@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No interface information available. Using loopback interface(s).");
|
_logger.LogWarning("No interface information available. Using loopback interface(s).");
|
||||||
|
|
||||||
if (IsIPv4Enabled && !IsIPv6Enabled)
|
if (IsIPv4Enabled)
|
||||||
{
|
{
|
||||||
interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo"));
|
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsIPv4Enabled && IsIPv6Enabled)
|
if (IsIPv6Enabled)
|
||||||
{
|
{
|
||||||
interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo"));
|
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialises internal LAN cache.
|
/// Initializes internal LAN cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitialiseLan(NetworkConfiguration config)
|
private void InitializeLan(NetworkConfiguration config)
|
||||||
{
|
{
|
||||||
lock (_initLock)
|
lock (_initLock)
|
||||||
{
|
{
|
||||||
|
@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
|
||||||
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
|
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
|
||||||
? excludedSubnets
|
? excludedSubnets
|
||||||
: new List<IPNetwork>();
|
: new List<IPNetwork>();
|
||||||
|
|
||||||
_logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
|
|
||||||
_logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
|
|
||||||
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
|
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
|
||||||
|
|
||||||
if (bindAddresses.Contains(IPAddress.Loopback))
|
if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
|
||||||
{
|
{
|
||||||
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
|
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bindAddresses.Contains(IPAddress.IPv6Loopback))
|
if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
|
||||||
{
|
{
|
||||||
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
|
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
|
||||||
}
|
}
|
||||||
|
@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
|
||||||
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
|
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
|
|
||||||
_interfaces = interfaces;
|
_interfaces = interfaces;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialises the remote address values.
|
/// Initializes the remote address values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitialiseRemote(NetworkConfiguration config)
|
private void InitializeRemote(NetworkConfiguration config)
|
||||||
{
|
{
|
||||||
lock (_initLock)
|
lock (_initLock)
|
||||||
{
|
{
|
||||||
|
@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
|
||||||
/// format is subnet=ipaddress|host|uri
|
/// format is subnet=ipaddress|host|uri
|
||||||
/// when subnet = 0.0.0.0, any external address matches.
|
/// when subnet = 0.0.0.0, any external address matches.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitialiseOverrides(NetworkConfiguration config)
|
private void InitializeOverrides(NetworkConfiguration config)
|
||||||
{
|
{
|
||||||
lock (_initLock)
|
lock (_initLock)
|
||||||
{
|
{
|
||||||
var publishedServerUrls = new Dictionary<IPData, string>();
|
var publishedServerUrls = new List<PublishedServerUriOverride>();
|
||||||
var overrides = config.PublishedServerUriBySubnet;
|
|
||||||
|
|
||||||
|
// Prefer startup configuration.
|
||||||
|
var startupOverrideKey = _startupConfig[AddressOverrideKey];
|
||||||
|
if (!string.IsNullOrEmpty(startupOverrideKey))
|
||||||
|
{
|
||||||
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
new IPData(IPAddress.Any, Network.IPv4Any),
|
||||||
|
startupOverrideKey,
|
||||||
|
true,
|
||||||
|
true));
|
||||||
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
|
||||||
|
startupOverrideKey,
|
||||||
|
true,
|
||||||
|
true));
|
||||||
|
_publishedServerUrls = publishedServerUrls;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var overrides = config.PublishedServerUriBySubnet;
|
||||||
foreach (var entry in overrides)
|
foreach (var entry in overrides)
|
||||||
{
|
{
|
||||||
var parts = entry.Split('=');
|
var parts = entry.Split('=');
|
||||||
|
@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
|
||||||
var identifier = parts[0];
|
var identifier = parts[0];
|
||||||
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement;
|
// Drop any other overrides in case an "all" override exists
|
||||||
|
publishedServerUrls.Clear();
|
||||||
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
new IPData(IPAddress.Any, Network.IPv4Any),
|
||||||
|
replacement,
|
||||||
|
true,
|
||||||
|
true));
|
||||||
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
|
||||||
|
replacement,
|
||||||
|
true,
|
||||||
|
true));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement;
|
publishedServerUrls.Add(
|
||||||
publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement;
|
new PublishedServerUriOverride(
|
||||||
|
new IPData(IPAddress.Any, Network.IPv4Any),
|
||||||
|
replacement,
|
||||||
|
false,
|
||||||
|
true));
|
||||||
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
|
||||||
|
replacement,
|
||||||
|
false,
|
||||||
|
true));
|
||||||
}
|
}
|
||||||
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
foreach (var lan in _lanSubnets)
|
foreach (var lan in _lanSubnets)
|
||||||
{
|
{
|
||||||
var lanPrefix = lan.Prefix;
|
var lanPrefix = lan.Prefix;
|
||||||
publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement;
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
|
||||||
|
replacement,
|
||||||
|
true,
|
||||||
|
false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
|
else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
|
||||||
{
|
{
|
||||||
var data = new IPData(result.Prefix, result);
|
var data = new IPData(result.Prefix, result);
|
||||||
publishedServerUrls[data] = replacement;
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
data,
|
||||||
|
replacement,
|
||||||
|
true,
|
||||||
|
true));
|
||||||
}
|
}
|
||||||
else if (TryParseInterface(identifier, out var ifaces))
|
else if (TryParseInterface(identifier, out var ifaces))
|
||||||
{
|
{
|
||||||
foreach (var iface in ifaces)
|
foreach (var iface in ifaces)
|
||||||
{
|
{
|
||||||
publishedServerUrls[iface] = replacement;
|
publishedServerUrls.Add(
|
||||||
|
new PublishedServerUriOverride(
|
||||||
|
iface,
|
||||||
|
replacement,
|
||||||
|
true,
|
||||||
|
true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reloads all settings and re-initialises the instance.
|
/// Reloads all settings and re-Initializes the instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
|
/// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
|
||||||
public void UpdateSettings(object configuration)
|
public void UpdateSettings(object configuration)
|
||||||
|
@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
|
||||||
var config = (NetworkConfiguration)configuration;
|
var config = (NetworkConfiguration)configuration;
|
||||||
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
|
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
|
||||||
|
|
||||||
InitialiseLan(config);
|
InitializeLan(config);
|
||||||
InitialiseRemote(config);
|
InitializeRemote(config);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(MockNetworkSettings))
|
if (string.IsNullOrEmpty(MockNetworkSettings))
|
||||||
{
|
{
|
||||||
InitialiseInterfaces();
|
InitializeInterfaces();
|
||||||
}
|
}
|
||||||
else // Used in testing only.
|
else // Used in testing only.
|
||||||
{
|
{
|
||||||
|
@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
|
||||||
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
||||||
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
|
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
{
|
{
|
||||||
var data = new IPData(address, subnet, parts[2]);
|
var data = new IPData(address, subnet, parts[2])
|
||||||
data.Index = index;
|
{
|
||||||
|
Index = index
|
||||||
|
};
|
||||||
interfaces.Add(data);
|
interfaces.Add(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
EnforceBindSettings(config);
|
EnforceBindSettings(config);
|
||||||
InitialiseOverrides(config);
|
InitializeOverrides(config);
|
||||||
|
|
||||||
|
PrintNetworkInformation(config, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
|
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
|
||||||
{
|
{
|
||||||
if (_interfaces.Count != 0)
|
if (_interfaces.Count > 0 || individualInterfaces)
|
||||||
{
|
{
|
||||||
return _interfaces;
|
return _interfaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No bind address and no exclusions, so listen on all interfaces.
|
// No bind address and no exclusions, so listen on all interfaces.
|
||||||
var result = new List<IPData>();
|
var result = new List<IPData>();
|
||||||
|
|
||||||
if (individualInterfaces)
|
|
||||||
{
|
|
||||||
result.AddRange(_interfaces);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsIPv4Enabled && IsIPv6Enabled)
|
if (IsIPv4Enabled && IsIPv6Enabled)
|
||||||
{
|
{
|
||||||
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
|
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
|
||||||
|
@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager
|
||||||
bindPreference = string.Empty;
|
bindPreference = string.Empty;
|
||||||
int? port = null;
|
int? port = null;
|
||||||
|
|
||||||
var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)
|
// Only consider subnets including the source IP, prefering specific overrides
|
||||||
|| x.Key.Address.Equals(IPAddress.IPv6Any)
|
List<PublishedServerUriOverride> validPublishedServerUrls;
|
||||||
|| x.Key.Subnet.Contains(source))
|
if (!isInExternalSubnet)
|
||||||
.DistinctBy(x => x.Key)
|
{
|
||||||
.OrderBy(x => x.Key.Address.Equals(IPAddress.Any)
|
// Only use matching internal subnets
|
||||||
|| x.Key.Address.Equals(IPAddress.IPv6Any))
|
// Prefer more specific (bigger subnet prefix) overrides
|
||||||
|
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
|
||||||
|
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Only use matching external subnets
|
||||||
|
// Prefer more specific (bigger subnet prefix) overrides
|
||||||
|
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
|
||||||
|
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
// Check for user override.
|
|
||||||
foreach (var data in validPublishedServerUrls)
|
foreach (var data in validPublishedServerUrls)
|
||||||
{
|
{
|
||||||
if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)))
|
// Get interface matching override subnet
|
||||||
{
|
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
|
||||||
// External.
|
|
||||||
bindPreference = data.Value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get address interface.
|
|
||||||
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address));
|
|
||||||
|
|
||||||
if (intf?.Address is not null)
|
if (intf?.Address is not null)
|
||||||
{
|
{
|
||||||
// Match IP address.
|
// If matching interface is found, use override
|
||||||
bindPreference = data.Value;
|
bindPreference = data.OverrideUri;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has it got a port defined?
|
// Handle override specifying port
|
||||||
var parts = bindPreference.Split(':');
|
var parts = bindPreference.Split(':');
|
||||||
if (parts.Length > 1)
|
if (parts.Length > 1)
|
||||||
{
|
{
|
||||||
|
@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
|
||||||
{
|
{
|
||||||
bindPreference = parts[0];
|
bindPreference = parts[0];
|
||||||
port = p;
|
port = p;
|
||||||
|
_logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port is not null)
|
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
|
||||||
{
|
|
||||||
_logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
|
||||||
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
|
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
|
||||||
|
{
|
||||||
|
var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
|
||||||
|
if (_logger.IsEnabled(logLevel))
|
||||||
|
{
|
||||||
|
_logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
|
||||||
|
_logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
|
||||||
|
_logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
|
||||||
|
_logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
|
||||||
|
_logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
|
||||||
|
_logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authorization.
|
/// Gets the authorization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpContext">The HTTP context.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
|
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var auth = GetAuthorizationDictionary(httpReq);
|
var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
|
||||||
var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
|
|
||||||
|
|
||||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||||
return authInfo;
|
return authInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
|
||||||
auth.TryGetValue("Token", out token);
|
auth.TryGetValue("Token", out token);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
|
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
token = headers["X-Emby-Token"];
|
token = headers["X-Emby-Token"];
|
||||||
|
@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
|
||||||
// Request doesn't contain a token.
|
// Request doesn't contain a token.
|
||||||
return authInfo;
|
return authInfo;
|
||||||
}
|
}
|
||||||
#pragma warning restore CA1508
|
|
||||||
|
|
||||||
authInfo.HasToken = true;
|
authInfo.HasToken = true;
|
||||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the auth.
|
/// Gets the auth.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpReq">The HTTP request.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
|
||||||
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
|
|
||||||
{
|
|
||||||
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(auth))
|
|
||||||
{
|
|
||||||
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the auth.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
||||||
{
|
{
|
||||||
|
|
|
@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions
|
||||||
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
|
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses))
|
else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
|
||||||
{
|
{
|
||||||
foreach (var address in addresses)
|
foreach (var address in addresses)
|
||||||
{
|
{
|
||||||
|
|
|
@ -35,7 +35,7 @@ public static class WebHostBuilderExtensions
|
||||||
return builder
|
return builder
|
||||||
.UseKestrel((builderContext, options) =>
|
.UseKestrel((builderContext, options) =>
|
||||||
{
|
{
|
||||||
var addresses = appHost.NetManager.GetAllBindInterfaces();
|
var addresses = appHost.NetManager.GetAllBindInterfaces(true);
|
||||||
|
|
||||||
bool flagged = false;
|
bool flagged = false;
|
||||||
foreach (var netAdd in addresses)
|
foreach (var netAdd in addresses)
|
||||||
|
|
|
@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="process">The process to wait for.</param>
|
/// <param name="process">The process to wait for.</param>
|
||||||
/// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
|
/// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
|
||||||
/// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns>
|
/// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
|
||||||
/// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
|
/// <exception cref="OperationCanceledException">The timeout ended.</exception>
|
||||||
public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
|
public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
|
||||||
{
|
{
|
||||||
using (var cancelTokenSource = new CancellationTokenSource(timeout))
|
using (var cancelTokenSource = new CancellationTokenSource(timeout))
|
||||||
{
|
{
|
||||||
return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false);
|
await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Asynchronously wait for the process to exit.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="process">The process to wait for.</param>
|
|
||||||
/// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
|
|
||||||
/// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
|
|
||||||
public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken)
|
|
||||||
{
|
|
||||||
if (!process.EnableRaisingEvents)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an event handler for the process exit event
|
|
||||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
process.Exited += (_, _) => tcs.TrySetResult(true);
|
|
||||||
|
|
||||||
// Return immediately if the process has already exited
|
|
||||||
if (process.HasExitedSafe())
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register with the cancellation token then await
|
|
||||||
using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe())))
|
|
||||||
{
|
|
||||||
return await tcs.Task.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the associated process has been terminated using
|
|
||||||
/// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
|
|
||||||
/// associated with the <see cref="Process"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="process">The process to check the exit status for.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the operating system process referenced by the <see cref="Process"/> component has
|
|
||||||
/// terminated, or if there is no associated operating system process; otherwise, false.
|
|
||||||
/// </returns>
|
|
||||||
private static bool HasExitedSafe(this Process process)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return process.HasExited;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,21 +35,15 @@ namespace MediaBrowser.Common
|
||||||
string SystemId { get; }
|
string SystemId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance has pending kernel reload.
|
/// Gets a value indicating whether this instance has pending changes requiring a restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
|
||||||
bool HasPendingRestart { get; }
|
bool HasPendingRestart { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance is currently shutting down.
|
/// Gets or sets a value indicating whether the application should restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
|
bool ShouldRestart { get; set; }
|
||||||
bool IsShuttingDown { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the application should restart.
|
|
||||||
/// </summary>
|
|
||||||
bool ShouldRestart { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the application version.
|
/// Gets the application version.
|
||||||
|
@ -91,11 +85,6 @@ namespace MediaBrowser.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void NotifyPendingRestart();
|
void NotifyPendingRestart();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Restarts this instance.
|
|
||||||
/// </summary>
|
|
||||||
void Restart();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the exports.
|
/// Gets the exports.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -127,11 +116,6 @@ namespace MediaBrowser.Common
|
||||||
/// <returns>``0.</returns>
|
/// <returns>``0.</returns>
|
||||||
T Resolve<T>();
|
T Resolve<T>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shuts down.
|
|
||||||
/// </summary>
|
|
||||||
void Shutdown();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes this instance.
|
/// Initializes this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -38,10 +38,6 @@
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||||
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
||||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
|
|
||||||
string? GetImageCacheTag(User user);
|
string? GetImageCacheTag(User user);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes the image.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">The options.</param>
|
|
||||||
/// <param name="toStream">To stream.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task ProcessImage(ImageProcessingOptions options, Stream toStream);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes the image.
|
/// Processes the image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
/// <param name="options">The options.</param>
|
/// <param name="options">The options.</param>
|
||||||
/// <param name="libraryName">The library name to draw onto the collage.</param>
|
/// <param name="libraryName">The library name to draw onto the collage.</param>
|
||||||
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
|
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
|
||||||
|
|
||||||
bool SupportsTransparency(string path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
private bool IsFormatSupported(string originalImagePath)
|
private bool IsFormatSupported(string originalImagePath)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(originalImagePath);
|
var ext = Path.GetExtension(originalImagePath);
|
||||||
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
|
ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
193
MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
Normal file
193
MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Xml;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for <see cref="XmlReader"/> to parse <see cref="BaseItem"/>'s.
|
||||||
|
/// </summary>
|
||||||
|
public static class XmlReaderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a trimmed string from the current node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||||
|
/// <returns>The trimmed content.</returns>
|
||||||
|
public static string ReadNormalizedString(this XmlReader reader)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reader);
|
||||||
|
|
||||||
|
return reader.ReadElementContentAsString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads an int from the current node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||||
|
/// <param name="value">The parsed <c>int</c>.</param>
|
||||||
|
/// <returns>A value indicating whether the parsing succeeded.</returns>
|
||||||
|
public static bool TryReadInt(this XmlReader reader, out int value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reader);
|
||||||
|
|
||||||
|
return int.TryParse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a <see cref="DateTime"/> from the current node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||||
|
/// <param name="value">The parsed <see cref="DateTime"/>.</param>
|
||||||
|
/// <returns>A value indicating whether the parsing succeeded.</returns>
|
||||||
|
public static bool TryReadDateTime(this XmlReader reader, out DateTime value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reader);
|
||||||
|
|
||||||
|
return DateTime.TryParse(
|
||||||
|
reader.ReadElementContentAsString(),
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||||
|
out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a <see cref="DateTime"/> from the current node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||||
|
/// <param name="formatString">The date format string.</param>
|
||||||
|
/// <param name="value">The parsed <see cref="DateTime"/>.</param>
|
||||||
|
/// <returns>A value indicating whether the parsing succeeded.</returns>
|
||||||
|
public static bool TryReadDateTimeExact(this XmlReader reader, string formatString, out DateTime value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reader);
|
||||||
|
ArgumentNullException.ThrowIfNull(formatString);
|
||||||
|
|
||||||
|
return DateTime.TryParseExact(
|
||||||
|
reader.ReadElementContentAsString(),
|
||||||
|
formatString,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||||
|
out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a <see cref="PersonInfo"/> from the xml node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||||
|
/// <returns>A <see cref="PersonInfo"/>, or <c>null</c> if none is found.</returns>
|
||||||
|
public static PersonInfo? GetPersonFromXmlNode(this XmlReader reader)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reader);
|
||||||
|
|
||||||
|
if (reader.IsEmptyElement)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = string.Empty;
|
||||||
|
var type = PersonKind.Actor; // If type is not specified assume actor
|
||||||
|
var role = string.Empty;
|
||||||
|
int? sortOrder = null;
|
||||||
|
string? imageUrl = null;
|
||||||
|
|
||||||
|
using var subtree = reader.ReadSubtree();
|
||||||
|
subtree.MoveToContent();
|
||||||
|
subtree.Read();
|
||||||
|
|
||||||
|
while (subtree is { EOF: false, ReadState: ReadState.Interactive })
|
||||||
|
{
|
||||||
|
if (subtree.NodeType != XmlNodeType.Element)
|
||||||
|
{
|
||||||
|
subtree.Read();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (subtree.Name)
|
||||||
|
{
|
||||||
|
case "name":
|
||||||
|
case "Name":
|
||||||
|
name = subtree.ReadNormalizedString();
|
||||||
|
break;
|
||||||
|
case "role":
|
||||||
|
case "Role":
|
||||||
|
role = subtree.ReadNormalizedString();
|
||||||
|
break;
|
||||||
|
case "type":
|
||||||
|
case "Type":
|
||||||
|
Enum.TryParse(subtree.ReadElementContentAsString(), true, out type);
|
||||||
|
break;
|
||||||
|
case "order":
|
||||||
|
case "sortorder":
|
||||||
|
case "SortOrder":
|
||||||
|
if (subtree.TryReadInt(out var sortOrderVal))
|
||||||
|
{
|
||||||
|
sortOrder = sortOrderVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "thumb":
|
||||||
|
imageUrl = subtree.ReadNormalizedString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
subtree.Skip();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PersonInfo
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Role = role,
|
||||||
|
Type = type,
|
||||||
|
SortOrder = sortOrder,
|
||||||
|
ImageUrl = imageUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to split names of comma or pipe delimited genres and people.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||||
|
/// <returns>IEnumerable{System.String}.</returns>
|
||||||
|
public static IEnumerable<string> GetStringArray(this XmlReader reader)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reader);
|
||||||
|
var value = reader.ReadElementContentAsString();
|
||||||
|
|
||||||
|
// Only split by comma if there is no pipe in the string
|
||||||
|
// We have to be careful to not split names like Matthew, Jr.
|
||||||
|
var separator = !value.Contains('|', StringComparison.Ordinal)
|
||||||
|
&& !value.Contains(';', StringComparison.Ordinal)
|
||||||
|
? new[] { ',' }
|
||||||
|
: new[] { '|', ';' };
|
||||||
|
|
||||||
|
foreach (var part in value.Trim().Trim(separator).Split(separator))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(part))
|
||||||
|
{
|
||||||
|
yield return part.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a <see cref="PersonInfo"/> array from the xml node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||||
|
/// <param name="personKind">The <see cref="PersonKind"/>.</param>
|
||||||
|
/// <returns>The <see cref="IEnumerable{PersonInfo}"/>.</returns>
|
||||||
|
public static IEnumerable<PersonInfo> GetPersonArray(this XmlReader reader, PersonKind personKind)
|
||||||
|
=> reader.GetStringArray()
|
||||||
|
.Select(part => new PersonInfo { Name = part, Type = personKind });
|
||||||
|
}
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller
|
namespace MediaBrowser.Controller
|
||||||
|
@ -16,8 +15,6 @@ namespace MediaBrowser.Controller
|
||||||
{
|
{
|
||||||
bool CoreStartupHasCompleted { get; }
|
bool CoreStartupHasCompleted { get; }
|
||||||
|
|
||||||
bool CanLaunchWebBrowser { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the HTTP server port.
|
/// Gets the HTTP server port.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -41,15 +38,6 @@ namespace MediaBrowser.Controller
|
||||||
/// <value>The name of the friendly.</value>
|
/// <value>The name of the friendly.</value>
|
||||||
string FriendlyName { get; }
|
string FriendlyName { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the system info.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The HTTP request.</param>
|
|
||||||
/// <returns>SystemInfo.</returns>
|
|
||||||
SystemInfo GetSystemInfo(HttpRequest request);
|
|
||||||
|
|
||||||
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a URL specific for the request.
|
/// Gets a URL specific for the request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
34
MediaBrowser.Controller/ISystemManager.cs
Normal file
34
MediaBrowser.Controller/ISystemManager.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using MediaBrowser.Model.System;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A service for managing the application instance.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISystemManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the system info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The HTTP request.</param>
|
||||||
|
/// <returns>The <see cref="SystemInfo"/>.</returns>
|
||||||
|
SystemInfo GetSystemInfo(HttpRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the public system info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The HTTP request.</param>
|
||||||
|
/// <returns>The <see cref="PublicSystemInfo"/>.</returns>
|
||||||
|
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the application restart process.
|
||||||
|
/// </summary>
|
||||||
|
void Restart();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the application shutdown process.
|
||||||
|
/// </summary>
|
||||||
|
void Shutdown();
|
||||||
|
}
|
|
@ -548,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
/// <returns>System.Nullable{VideoCodecs}.</returns>
|
/// <returns>System.Nullable{VideoCodecs}.</returns>
|
||||||
public string InferVideoCodec(string url)
|
public string InferVideoCodec(string url)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(url);
|
var ext = Path.GetExtension(url.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "wmv";
|
return "wmv";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// TODO: this may not always mean VP8, as the codec ages
|
// TODO: this may not always mean VP8, as the codec ages
|
||||||
return "vp8";
|
return "vp8";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "theora";
|
return "theora";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "h264";
|
return "h264";
|
||||||
}
|
}
|
||||||
|
@ -1080,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
&& state.SubtitleStream.IsExternal)
|
&& state.SubtitleStream.IsExternal)
|
||||||
{
|
{
|
||||||
var subtitlePath = state.SubtitleStream.Path;
|
var subtitlePath = state.SubtitleStream.Path;
|
||||||
var subtitleExtension = Path.GetExtension(subtitlePath);
|
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase)
|
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase))
|
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
||||||
if (File.Exists(idxFile))
|
if (File.Exists(idxFile))
|
||||||
|
@ -6038,7 +6038,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
var format = string.Empty;
|
var format = string.Empty;
|
||||||
var keyFrame = string.Empty;
|
var keyFrame = string.Empty;
|
||||||
|
|
||||||
if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase)
|
if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
|
||||||
&& state.BaseRequest.Context == EncodingContext.Streaming)
|
&& state.BaseRequest.Context == EncodingContext.Streaming)
|
||||||
{
|
{
|
||||||
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
||||||
|
|
|
@ -9,6 +9,7 @@ using System.Xml;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
@ -128,42 +129,19 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
|
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
// DateCreated
|
|
||||||
case "Added":
|
case "Added":
|
||||||
{
|
if (reader.TryReadDateTime(out var dateCreated))
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
{
|
||||||
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
|
item.DateCreated = dateCreated;
|
||||||
{
|
|
||||||
item.DateCreated = added;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.LogWarning("Invalid Added value found: {Value}", val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "OriginalTitle":
|
case "OriginalTitle":
|
||||||
{
|
item.OriginalTitle = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(val))
|
|
||||||
{
|
|
||||||
item.OriginalTitle = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "LocalTitle":
|
case "LocalTitle":
|
||||||
item.Name = reader.ReadElementContentAsString();
|
item.Name = reader.ReadNormalizedString();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "CriticRating":
|
case "CriticRating":
|
||||||
{
|
{
|
||||||
var text = reader.ReadElementContentAsString();
|
var text = reader.ReadElementContentAsString();
|
||||||
|
@ -177,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
}
|
}
|
||||||
|
|
||||||
case "SortTitle":
|
case "SortTitle":
|
||||||
{
|
item.ForcedSortName = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
item.ForcedSortName = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Overview":
|
case "Overview":
|
||||||
case "Description":
|
case "Description":
|
||||||
{
|
item.Overview = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
item.Overview = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Language":
|
case "Language":
|
||||||
{
|
item.PreferredMetadataLanguage = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
item.PreferredMetadataLanguage = val;
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "CountryCode":
|
case "CountryCode":
|
||||||
{
|
item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
item.PreferredMetadataCountryCode = val;
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "PlaceOfBirth":
|
case "PlaceOfBirth":
|
||||||
{
|
var placeOfBirth = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person)
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
{
|
||||||
if (item is Person person)
|
person.ProductionLocations = new[] { placeOfBirth };
|
||||||
{
|
|
||||||
person.ProductionLocations = new[] { val };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "LockedFields":
|
case "LockedFields":
|
||||||
{
|
{
|
||||||
var val = reader.ReadElementContentAsString();
|
var val = reader.ReadElementContentAsString();
|
||||||
|
@ -275,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
{
|
{
|
||||||
if (!reader.IsEmptyElement)
|
if (!reader.IsEmptyElement)
|
||||||
{
|
{
|
||||||
using (var subtree = reader.ReadSubtree())
|
reader.Skip();
|
||||||
{
|
|
||||||
FetchFromCountriesNode(subtree);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -290,183 +228,84 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
|
|
||||||
case "ContentRating":
|
case "ContentRating":
|
||||||
case "MPAARating":
|
case "MPAARating":
|
||||||
{
|
item.OfficialRating = reader.ReadNormalizedString();
|
||||||
var rating = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(rating))
|
|
||||||
{
|
|
||||||
item.OfficialRating = rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "CustomRating":
|
case "CustomRating":
|
||||||
{
|
item.CustomRating = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
item.CustomRating = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "RunningTime":
|
case "RunningTime":
|
||||||
{
|
var runtimeText = reader.ReadElementContentAsString();
|
||||||
var text = reader.ReadElementContentAsString();
|
if (!string.IsNullOrWhiteSpace(runtimeText))
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
{
|
||||||
if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
|
if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
|
||||||
{
|
{
|
||||||
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
|
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "AspectRatio":
|
case "AspectRatio":
|
||||||
{
|
var aspectRatio = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio)
|
|
||||||
{
|
{
|
||||||
hasAspectRatio.AspectRatio = val;
|
hasAspectRatio.AspectRatio = aspectRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "LockData":
|
case "LockData":
|
||||||
{
|
item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Network":
|
case "Network":
|
||||||
{
|
foreach (var name in reader.GetStringArray())
|
||||||
foreach (var name in SplitNames(reader.ReadElementContentAsString()))
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.AddStudio(name);
|
item.AddStudio(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Director":
|
case "Director":
|
||||||
{
|
foreach (var director in reader.GetPersonArray(PersonKind.Director))
|
||||||
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(p.Name))
|
itemResult.AddPerson(director);
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemResult.AddPerson(p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Writer":
|
case "Writer":
|
||||||
{
|
foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
|
||||||
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(p.Name))
|
itemResult.AddPerson(writer);
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemResult.AddPerson(p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Actors":
|
case "Actors":
|
||||||
{
|
foreach (var actor in reader.GetPersonArray(PersonKind.Actor))
|
||||||
var actors = reader.ReadInnerXml();
|
|
||||||
|
|
||||||
if (actors.Contains('<', StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
// This is one of the mis-named "Actors" full nodes created by MB2
|
itemResult.AddPerson(actor);
|
||||||
// Create a reader and pass it to the persons node processor
|
|
||||||
using var xmlReader = XmlReader.Create(new StringReader($"<Persons>{actors}</Persons>"));
|
|
||||||
FetchDataFromPersonsNode(xmlReader, itemResult);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Old-style piped string
|
|
||||||
foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor }))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(p.Name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemResult.AddPerson(p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "GuestStars":
|
case "GuestStars":
|
||||||
{
|
foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar))
|
||||||
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(p.Name))
|
itemResult.AddPerson(guestStar);
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemResult.AddPerson(p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Trailer":
|
case "Trailer":
|
||||||
{
|
var trailer = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(trailer))
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
{
|
||||||
item.AddTrailerUrl(val);
|
item.AddTrailerUrl(trailer);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "DisplayOrder":
|
case "DisplayOrder":
|
||||||
{
|
var displayOrder = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
|
||||||
|
|
||||||
if (item is IHasDisplayOrder hasDisplayOrder)
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
hasDisplayOrder.DisplayOrder = displayOrder;
|
||||||
{
|
|
||||||
hasDisplayOrder.DisplayOrder = val;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Trailers":
|
case "Trailers":
|
||||||
{
|
{
|
||||||
if (!reader.IsEmptyElement)
|
if (!reader.IsEmptyElement)
|
||||||
|
@ -483,20 +322,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ProductionYear":
|
case "ProductionYear":
|
||||||
{
|
if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
{
|
||||||
if (int.TryParse(val, out var productionYear) && productionYear > 1850)
|
item.ProductionYear = productionYear;
|
||||||
{
|
|
||||||
item.ProductionYear = productionYear;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "Rating":
|
case "Rating":
|
||||||
case "IMDBrating":
|
case "IMDBrating":
|
||||||
{
|
{
|
||||||
|
@ -517,40 +348,24 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
case "BirthDate":
|
case "BirthDate":
|
||||||
case "PremiereDate":
|
case "PremiereDate":
|
||||||
case "FirstAired":
|
case "FirstAired":
|
||||||
{
|
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired))
|
||||||
var firstAired = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(firstAired))
|
|
||||||
{
|
{
|
||||||
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
|
item.PremiereDate = firstAired;
|
||||||
{
|
item.ProductionYear = firstAired.Year;
|
||||||
item.PremiereDate = airDate;
|
|
||||||
item.ProductionYear = airDate.Year;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "DeathDate":
|
case "DeathDate":
|
||||||
case "EndDate":
|
case "EndDate":
|
||||||
{
|
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate))
|
||||||
var firstAired = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(firstAired))
|
|
||||||
{
|
{
|
||||||
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
|
item.EndDate = endDate;
|
||||||
{
|
|
||||||
item.EndDate = airDate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "CollectionNumber":
|
case "CollectionNumber":
|
||||||
var tmdbCollection = reader.ReadElementContentAsString();
|
var tmdbCollection = reader.ReadNormalizedString();
|
||||||
if (!string.IsNullOrWhiteSpace(tmdbCollection))
|
if (!string.IsNullOrEmpty(tmdbCollection))
|
||||||
{
|
{
|
||||||
item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
|
item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
|
||||||
}
|
}
|
||||||
|
@ -753,41 +568,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
item.Shares = list.ToArray();
|
item.Shares = list.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FetchFromCountriesNode(XmlReader reader)
|
|
||||||
{
|
|
||||||
reader.MoveToContent();
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
// Loop through each element
|
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
|
||||||
{
|
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
|
||||||
{
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "Country":
|
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
reader.Skip();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches from taglines node.
|
/// Fetches from taglines node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -806,17 +586,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "Tagline":
|
case "Tagline":
|
||||||
{
|
item.Tagline = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
item.Tagline = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
@ -847,17 +618,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "Genre":
|
case "Genre":
|
||||||
{
|
var genre = reader.ReadNormalizedString();
|
||||||
var genre = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(genre))
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(genre))
|
|
||||||
{
|
{
|
||||||
item.AddGenre(genre);
|
item.AddGenre(genre);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
@ -885,17 +652,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "Tag":
|
case "Tag":
|
||||||
{
|
var tag = reader.ReadNormalizedString();
|
||||||
var tag = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(tag))
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tag))
|
|
||||||
{
|
{
|
||||||
tags.Add(tag);
|
tags.Add(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
@ -929,29 +692,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
{
|
{
|
||||||
case "Person":
|
case "Person":
|
||||||
case "Actor":
|
case "Actor":
|
||||||
{
|
var person = reader.GetPersonFromXmlNode();
|
||||||
if (reader.IsEmptyElement)
|
if (person is not null)
|
||||||
{
|
{
|
||||||
reader.Read();
|
item.AddPerson(person);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var subtree = reader.ReadSubtree())
|
|
||||||
{
|
|
||||||
foreach (var person in GetPersonsFromXmlNode(subtree))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(person.Name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.AddPerson(person);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
@ -977,17 +724,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "Trailer":
|
case "Trailer":
|
||||||
{
|
var trailer = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(trailer))
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
{
|
||||||
item.AddTrailerUrl(val);
|
item.AddTrailerUrl(trailer);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
@ -1018,17 +761,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "Studio":
|
case "Studio":
|
||||||
{
|
var studio = reader.ReadNormalizedString();
|
||||||
var studio = reader.ReadElementContentAsString();
|
if (!string.IsNullOrEmpty(studio))
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(studio))
|
|
||||||
{
|
{
|
||||||
item.AddStudio(studio);
|
item.AddStudio(studio);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
@ -1041,83 +780,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the persons from XML node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reader">The reader.</param>
|
|
||||||
/// <returns>IEnumerable{PersonInfo}.</returns>
|
|
||||||
private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
|
|
||||||
{
|
|
||||||
var name = string.Empty;
|
|
||||||
var type = PersonKind.Actor; // If type is not specified assume actor
|
|
||||||
var role = string.Empty;
|
|
||||||
int? sortOrder = null;
|
|
||||||
|
|
||||||
reader.MoveToContent();
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
// Loop through each element
|
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
|
||||||
{
|
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
|
||||||
{
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "Name":
|
|
||||||
name = reader.ReadElementContentAsString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Type":
|
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
_ = Enum.TryParse(val, true, out type);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Role":
|
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
role = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "SortOrder":
|
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
|
|
||||||
{
|
|
||||||
sortOrder = intVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
reader.Skip();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var personInfo = new PersonInfo { Name = name.Trim(), Role = role, Type = type, SortOrder = sortOrder };
|
|
||||||
|
|
||||||
return new[] { personInfo };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get linked child.
|
/// Get linked child.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1138,17 +800,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "Path":
|
case "Path":
|
||||||
{
|
linkedItem.Path = reader.ReadNormalizedString();
|
||||||
linkedItem.Path = reader.ReadElementContentAsString();
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "ItemId":
|
case "ItemId":
|
||||||
{
|
linkedItem.LibraryItemId = reader.ReadNormalizedString();
|
||||||
linkedItem.LibraryItemId = reader.ReadElementContentAsString();
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
@ -1189,22 +845,14 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "UserId":
|
case "UserId":
|
||||||
{
|
item.UserId = reader.ReadNormalizedString();
|
||||||
item.UserId = reader.ReadElementContentAsString();
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "CanEdit":
|
case "CanEdit":
|
||||||
{
|
|
||||||
item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
|
item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -1221,34 +869,5 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used to split names of comma or pipe delimited genres and people.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
/// <returns>IEnumerable{System.String}.</returns>
|
|
||||||
private IEnumerable<string> SplitNames(string value)
|
|
||||||
{
|
|
||||||
// Only split by comma if there is no pipe in the string
|
|
||||||
// We have to be careful to not split names like Matthew, Jr.
|
|
||||||
var separator = !value.Contains('|', StringComparison.Ordinal)
|
|
||||||
&& !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' };
|
|
||||||
|
|
||||||
value = value.Trim().Trim(separator);
|
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides an additional overload for string.split.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val">The val.</param>
|
|
||||||
/// <param name="separators">The separators.</param>
|
|
||||||
/// <param name="options">The options.</param>
|
|
||||||
/// <returns>System.String[][].</returns>
|
|
||||||
private string[] Split(string val, char[] separators, StringSplitOptions options)
|
|
||||||
{
|
|
||||||
return val.Split(separators, options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "PlaylistMediaType":
|
case "PlaylistMediaType":
|
||||||
{
|
item.PlaylistMediaType = reader.ReadNormalizedString();
|
||||||
item.PlaylistMediaType = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "PlaylistItems":
|
case "PlaylistItems":
|
||||||
|
|
||||||
if (!reader.IsEmptyElement)
|
if (!reader.IsEmptyElement)
|
||||||
|
@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.MediaEncoding.Attachments
|
namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
{
|
{
|
||||||
public class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
public sealed class AttachmentExtractor : IAttachmentExtractor
|
||||||
{
|
{
|
||||||
private readonly ILogger<AttachmentExtractor> _logger;
|
private readonly ILogger<AttachmentExtractor> _logger;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
|
@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public AttachmentExtractor(
|
public AttachmentExtractor(
|
||||||
ILogger<AttachmentExtractor> logger,
|
ILogger<AttachmentExtractor> logger,
|
||||||
IApplicationPaths appPaths,
|
IApplicationPaths appPaths,
|
||||||
|
@ -177,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
|
try
|
||||||
|
|
||||||
if (!ranToCompletion)
|
|
||||||
{
|
{
|
||||||
try
|
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
{
|
exitCode = process.ExitCode;
|
||||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
}
|
||||||
process.Kill();
|
catch (OperationCanceledException)
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
process.Kill(true);
|
||||||
{
|
exitCode = -1;
|
||||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var failed = false;
|
var failed = false;
|
||||||
|
@ -296,7 +287,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
|
||||||
|
|
||||||
var processArgs = string.Format(
|
var processArgs = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -325,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
|
try
|
||||||
|
|
||||||
if (!ranToCompletion)
|
|
||||||
{
|
{
|
||||||
try
|
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
{
|
exitCode = process.ExitCode;
|
||||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
}
|
||||||
process.Kill();
|
catch (OperationCanceledException)
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
process.Kill(true);
|
||||||
{
|
exitCode = -1;
|
||||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var failed = false;
|
var failed = false;
|
||||||
|
@ -391,33 +376,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefix = filename.Substring(0, 1);
|
var prefix = filename.AsSpan(0, 1);
|
||||||
return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
|
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
{
|
{
|
||||||
var files = _fileSystem.GetFilePaths(path, recursive);
|
var files = _fileSystem.GetFilePaths(path, recursive);
|
||||||
|
|
||||||
var excludeExtensions = new[] { ".c" };
|
return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase));
|
||||||
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
@ -650,15 +648,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||||
|
|
||||||
var outputExtension = targetFormat switch
|
var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
|
||||||
{
|
|
||||||
ImageFormat.Bmp => ".bmp",
|
|
||||||
ImageFormat.Gif => ".gif",
|
|
||||||
ImageFormat.Jpg => ".jpg",
|
|
||||||
ImageFormat.Png => ".png",
|
|
||||||
ImageFormat.Webp => ".webp",
|
|
||||||
_ => ".jpg"
|
|
||||||
};
|
|
||||||
|
|
||||||
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
|
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
||||||
|
@ -760,11 +750,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
|
timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
|
try
|
||||||
|
|
||||||
if (!ranToCompletion)
|
|
||||||
{
|
{
|
||||||
StopProcess(processWrapper, 1000);
|
await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
|
||||||
|
ranToCompletion = true;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
process.Kill(true);
|
||||||
|
ranToCompletion = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
@ -999,7 +993,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ProcessWrapper : IDisposable
|
private sealed class ProcessWrapper : IDisposable
|
||||||
{
|
{
|
||||||
private readonly MediaEncoder _mediaEncoder;
|
private readonly MediaEncoder _mediaEncoder;
|
||||||
|
|
||||||
|
@ -1042,13 +1036,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
_mediaEncoder._runningProcesses.Remove(this);
|
_mediaEncoder._runningProcesses.Remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
process.Dispose();
|
||||||
{
|
|
||||||
process.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
@ -420,23 +420,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
try
|
||||||
|
|
||||||
if (!ranToCompletion)
|
|
||||||
{
|
{
|
||||||
try
|
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||||
{
|
exitCode = process.ExitCode;
|
||||||
_logger.LogInformation("Killing ffmpeg subtitle conversion process");
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
process.Kill();
|
{
|
||||||
}
|
process.Kill(true);
|
||||||
catch (Exception ex)
|
exitCode = -1;
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error killing subtitle conversion process");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var failed = false;
|
var failed = false;
|
||||||
|
@ -574,23 +567,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
try
|
||||||
|
|
||||||
if (!ranToCompletion)
|
|
||||||
{
|
{
|
||||||
try
|
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||||
{
|
exitCode = process.ExitCode;
|
||||||
_logger.LogWarning("Killing ffmpeg subtitle extraction process");
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
process.Kill();
|
{
|
||||||
}
|
process.Kill(true);
|
||||||
catch (Exception ex)
|
exitCode = -1;
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error killing subtitle extraction process");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var failed = false;
|
var failed = false;
|
||||||
|
|
|
@ -24,4 +24,21 @@ public static class ImageFormatExtensions
|
||||||
ImageFormat.Webp => "image/webp",
|
ImageFormat.Webp => "image/webp",
|
||||||
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the correct extension for this <see cref="ImageFormat" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">This <see cref="ImageFormat" />.</param>
|
||||||
|
/// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception>
|
||||||
|
/// <returns>The correct extension for this <see cref="ImageFormat" />.</returns>
|
||||||
|
public static string GetExtension(this ImageFormat format)
|
||||||
|
=> format switch
|
||||||
|
{
|
||||||
|
ImageFormat.Bmp => ".bmp",
|
||||||
|
ImageFormat.Gif => ".gif",
|
||||||
|
ImageFormat.Jpg => ".jpg",
|
||||||
|
ImageFormat.Png => ".png",
|
||||||
|
ImageFormat.Webp => ".webp",
|
||||||
|
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,11 @@ public class IPData
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the network supports multicast.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsMulticast { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the interface name.
|
/// Gets or sets the interface name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
42
MediaBrowser.Model/Net/PublishedServerUriOverride.cs
Normal file
42
MediaBrowser.Model/Net/PublishedServerUriOverride.cs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
namespace MediaBrowser.Model.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class holding information for a published server URI override.
|
||||||
|
/// </summary>
|
||||||
|
public class PublishedServerUriOverride
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PublishedServerUriOverride"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The <see cref="IPData"/>.</param>
|
||||||
|
/// <param name="overrideUri">The override.</param>
|
||||||
|
/// <param name="internalOverride">A value indicating whether the override is for internal requests.</param>
|
||||||
|
/// <param name="externalOverride">A value indicating whether the override is for external requests.</param>
|
||||||
|
public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
OverrideUri = overrideUri;
|
||||||
|
IsInternalOverride = internalOverride;
|
||||||
|
IsExternalOverride = externalOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the object's IP address.
|
||||||
|
/// </summary>
|
||||||
|
public IPData Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the override URI.
|
||||||
|
/// </summary>
|
||||||
|
public string OverrideUri { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the override should be applied to internal requests.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsInternalOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the override should be applied to external requests.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsExternalOverride { get; set; }
|
||||||
|
}
|
|
@ -84,7 +84,8 @@ namespace MediaBrowser.Model.System
|
||||||
[Obsolete("This is always true")]
|
[Obsolete("This is always true")]
|
||||||
public bool CanSelfRestart { get; set; } = true;
|
public bool CanSelfRestart { get; set; } = true;
|
||||||
|
|
||||||
public bool CanLaunchWebBrowser { get; set; }
|
[Obsolete("This is always false")]
|
||||||
|
public bool CanLaunchWebBrowser { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the program data path.
|
/// Gets or sets the program data path.
|
||||||
|
|
|
@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager
|
||||||
|
|
||||||
var fileStreamOptions = AsyncFile.WriteOptions;
|
var fileStreamOptions = AsyncFile.WriteOptions;
|
||||||
fileStreamOptions.Mode = FileMode.Create;
|
fileStreamOptions.Mode = FileMode.Create;
|
||||||
fileStreamOptions.PreallocationSize = source.Length;
|
if (source.CanSeek)
|
||||||
|
{
|
||||||
|
fileStreamOptions.PreallocationSize = source.Length;
|
||||||
|
}
|
||||||
|
|
||||||
var fs = new FileStream(path, fileStreamOptions);
|
var fs = new FileStream(path, fileStreamOptions);
|
||||||
await using (fs.ConfigureAwait(false))
|
await using (fs.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
|
|
@ -943,6 +943,12 @@ namespace MediaBrowser.Providers.Manager
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
|
public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(itemId);
|
||||||
|
if (itemId.Equals(default))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Guid can't be empty", nameof(itemId));
|
||||||
|
}
|
||||||
|
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
? Path.GetExtension(attachmentStream.FileName)
|
? Path.GetExtension(attachmentStream.FileName)
|
||||||
: MimeTypes.ToExtension(attachmentStream.MimeType);
|
: MimeTypes.ToExtension(attachmentStream.MimeType);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(extension))
|
|
||||||
{
|
|
||||||
extension = ".jpg";
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageFormat format = extension switch
|
ImageFormat format = extension switch
|
||||||
{
|
{
|
||||||
".bmp" => ImageFormat.Bmp,
|
".bmp" => ImageFormat.Bmp,
|
||||||
".gif" => ImageFormat.Gif,
|
".gif" => ImageFormat.Gif,
|
||||||
".jpg" => ImageFormat.Jpg,
|
|
||||||
".png" => ImageFormat.Png,
|
".png" => ImageFormat.Png,
|
||||||
".webp" => ImageFormat.Webp,
|
".webp" => ImageFormat.Webp,
|
||||||
_ => ImageFormat.Jpg
|
_ => ImageFormat.Jpg
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the last episode number from nfo
|
// Extract the last episode number from nfo
|
||||||
|
// Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
|
||||||
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
|
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
|
||||||
|
var name = new StringBuilder(item.Item.Name);
|
||||||
|
var overview = new StringBuilder(item.Item.Overview);
|
||||||
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
|
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
|
||||||
{
|
{
|
||||||
xml = xmlFile.Substring(0, index + srch.Length);
|
xml = xmlFile.Substring(0, index + srch.Length);
|
||||||
|
@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||||
{
|
{
|
||||||
reader.MoveToContent();
|
reader.MoveToContent();
|
||||||
|
|
||||||
if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
|
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||||
{
|
{
|
||||||
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (reader.NodeType == XmlNodeType.Element)
|
||||||
|
{
|
||||||
|
switch (reader.Name)
|
||||||
|
{
|
||||||
|
case "name":
|
||||||
|
case "title":
|
||||||
|
case "localtitle":
|
||||||
|
name.Append(" / ").Append(reader.ReadElementContentAsString());
|
||||||
|
break;
|
||||||
|
case "episode":
|
||||||
|
{
|
||||||
|
if (int.TryParse(reader.ReadElementContentAsString(), out var num))
|
||||||
|
{
|
||||||
|
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "biography":
|
||||||
|
case "plot":
|
||||||
|
case "review":
|
||||||
|
overview.Append(" / ").Append(reader.ReadElementContentAsString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Read();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.Item.Name = name.ToString();
|
||||||
|
item.Item.Overview = overview.ToString();
|
||||||
}
|
}
|
||||||
catch (XmlException)
|
catch (XmlException)
|
||||||
{
|
{
|
||||||
|
@ -112,142 +148,53 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "season":
|
case "season":
|
||||||
|
if (reader.TryReadInt(out var seasonNumber))
|
||||||
{
|
{
|
||||||
var number = reader.ReadElementContentAsString();
|
item.ParentIndexNumber = seasonNumber;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(number))
|
|
||||||
{
|
|
||||||
if (int.TryParse(number, out var num))
|
|
||||||
{
|
|
||||||
item.ParentIndexNumber = num;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
case "episode":
|
case "episode":
|
||||||
|
if (reader.TryReadInt(out var episodeNumber))
|
||||||
{
|
{
|
||||||
var number = reader.ReadElementContentAsString();
|
item.IndexNumber = episodeNumber;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(number))
|
|
||||||
{
|
|
||||||
if (int.TryParse(number, out var num))
|
|
||||||
{
|
|
||||||
item.IndexNumber = num;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
case "episodenumberend":
|
case "episodenumberend":
|
||||||
|
if (reader.TryReadInt(out var episodeNumberEnd))
|
||||||
{
|
{
|
||||||
var number = reader.ReadElementContentAsString();
|
item.IndexNumberEnd = episodeNumberEnd;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(number))
|
|
||||||
{
|
|
||||||
if (int.TryParse(number, out var num))
|
|
||||||
{
|
|
||||||
item.IndexNumberEnd = num;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
case "airsbefore_episode":
|
case "airsbefore_episode":
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
// int.TryParse is local aware, so it can be problematic, force us culture
|
|
||||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
|
|
||||||
{
|
|
||||||
item.AirsBeforeEpisodeNumber = rval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "airsafter_season":
|
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
// int.TryParse is local aware, so it can be problematic, force us culture
|
|
||||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
|
|
||||||
{
|
|
||||||
item.AirsAfterSeasonNumber = rval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "airsbefore_season":
|
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
// int.TryParse is local aware, so it can be problematic, force us culture
|
|
||||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
|
|
||||||
{
|
|
||||||
item.AirsBeforeSeasonNumber = rval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "displayseason":
|
|
||||||
{
|
|
||||||
var val = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
// int.TryParse is local aware, so it can be problematic, force us culture
|
|
||||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
|
|
||||||
{
|
|
||||||
item.AirsBeforeSeasonNumber = rval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "displayepisode":
|
case "displayepisode":
|
||||||
|
if (reader.TryReadInt(out var airsBeforeEpisode))
|
||||||
{
|
{
|
||||||
var val = reader.ReadElementContentAsString();
|
item.AirsBeforeEpisodeNumber = airsBeforeEpisode;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
// int.TryParse is local aware, so it can be problematic, force us culture
|
|
||||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
|
|
||||||
{
|
|
||||||
item.AirsBeforeEpisodeNumber = rval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "airsafter_season":
|
||||||
|
case "displayafterseason":
|
||||||
|
if (reader.TryReadInt(out var airsAfterSeason))
|
||||||
|
{
|
||||||
|
item.AirsAfterSeasonNumber = airsAfterSeason;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "airsbefore_season":
|
||||||
|
case "displayseason":
|
||||||
|
if (reader.TryReadInt(out var airsBeforeSeason))
|
||||||
|
{
|
||||||
|
item.AirsBeforeSeasonNumber = airsBeforeSeason;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
case "showtitle":
|
case "showtitle":
|
||||||
{
|
item.SeriesName = reader.ReadNormalizedString();
|
||||||
var showtitle = reader.ReadElementContentAsString();
|
break;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(showtitle))
|
|
||||||
{
|
|
||||||
item.SeriesName = showtitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
base.FetchDataFromXmlNode(reader, itemResult);
|
base.FetchDataFromXmlNode(reader, itemResult);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System.Xml;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
@ -113,31 +114,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||||
}
|
}
|
||||||
|
|
||||||
case "artist":
|
case "artist":
|
||||||
|
var artist = reader.ReadNormalizedString();
|
||||||
|
if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
|
||||||
{
|
{
|
||||||
var val = reader.ReadElementContentAsString();
|
var list = artistVideo.Artists.ToList();
|
||||||
|
list.Add(artist);
|
||||||
if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
|
artistVideo.Artists = list.ToArray();
|
||||||
{
|
|
||||||
var list = movie.Artists.ToList();
|
|
||||||
list.Add(val);
|
|
||||||
movie.Artists = list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
case "album":
|
case "album":
|
||||||
|
var album = reader.ReadNormalizedString();
|
||||||
|
if (!string.IsNullOrEmpty(album) && item is MusicVideo albumVideo)
|
||||||
{
|
{
|
||||||
var val = reader.ReadElementContentAsString();
|
albumVideo.Album = album;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
|
|
||||||
{
|
|
||||||
movie.Album = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
base.FetchDataFromXmlNode(reader, itemResult);
|
base.FetchDataFromXmlNode(reader, itemResult);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System.Globalization;
|
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -41,32 +41,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "seasonnumber":
|
case "seasonnumber":
|
||||||
|
if (reader.TryReadInt(out var seasonNumber))
|
||||||
{
|
{
|
||||||
var number = reader.ReadElementContentAsString();
|
item.IndexNumber = seasonNumber;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(number))
|
|
||||||
{
|
|
||||||
if (int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
|
|
||||||
{
|
|
||||||
item.IndexNumber = num;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
case "seasonname":
|
case "seasonname":
|
||||||
{
|
item.Name = reader.ReadNormalizedString();
|
||||||
var name = reader.ReadElementContentAsString();
|
break;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
item.Name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
base.FetchDataFromXmlNode(reader, itemResult);
|
base.FetchDataFromXmlNode(reader, itemResult);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Globalization;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
@ -75,23 +76,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||||
}
|
}
|
||||||
|
|
||||||
case "airs_dayofweek":
|
case "airs_dayofweek":
|
||||||
{
|
item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
|
||||||
item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "airs_time":
|
case "airs_time":
|
||||||
{
|
item.AirTime = reader.ReadNormalizedString();
|
||||||
var val = reader.ReadElementContentAsString();
|
break;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
item.AirTime = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "status":
|
case "status":
|
||||||
{
|
{
|
||||||
var status = reader.ReadElementContentAsString();
|
var status = reader.ReadElementContentAsString();
|
||||||
|
|
|
@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
yield return Path.ChangeExtension(item.Path, ".nfo");
|
|
||||||
|
|
||||||
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
||||||
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
||||||
{
|
{
|
||||||
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
|
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield return Path.ChangeExtension(item.Path, ".nfo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure
|
||||||
* port to use, we will default to 0 which allows the underlying system to auto-assign a free port.
|
* port to use, we will default to 0 which allows the underlying system to auto-assign a free port.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private object _BroadcastListenSocketSynchroniser = new object();
|
private object _BroadcastListenSocketSynchroniser = new();
|
||||||
private List<Socket> _MulticastListenSockets;
|
private List<Socket> _MulticastListenSockets;
|
||||||
|
|
||||||
private object _SendSocketSynchroniser = new object();
|
private object _SendSocketSynchroniser = new();
|
||||||
private List<Socket> _sendSockets;
|
private List<Socket> _sendSockets;
|
||||||
|
|
||||||
private HttpRequestParser _RequestParser;
|
private HttpRequestParser _RequestParser;
|
||||||
|
@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure
|
||||||
private int _MulticastTtl;
|
private int _MulticastTtl;
|
||||||
|
|
||||||
private bool _IsShared;
|
private bool _IsShared;
|
||||||
private readonly bool _enableMultiSocketBinding;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised when a HTTPU request message is received by a socket (unicast or multicast).
|
/// Raised when a HTTPU request message is received by a socket (unicast or multicast).
|
||||||
|
@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure
|
||||||
/// Minimum constructor.
|
/// Minimum constructor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
|
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
|
||||||
public SsdpCommunicationsServer(ISocketFactory socketFactory,
|
public SsdpCommunicationsServer(
|
||||||
INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
|
ISocketFactory socketFactory,
|
||||||
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
|
INetworkManager networkManager,
|
||||||
|
ILogger logger)
|
||||||
|
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
|
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
|
||||||
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception>
|
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception>
|
||||||
public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
|
public SsdpCommunicationsServer(
|
||||||
|
ISocketFactory socketFactory,
|
||||||
|
int localPort,
|
||||||
|
int multicastTimeToLive,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
ILogger logger)
|
||||||
{
|
{
|
||||||
if (socketFactory is null)
|
if (socketFactory is null)
|
||||||
{
|
{
|
||||||
|
@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure
|
||||||
throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero.");
|
throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_BroadcastListenSocketSynchroniser = new object();
|
_BroadcastListenSocketSynchroniser = new();
|
||||||
_SendSocketSynchroniser = new object();
|
_SendSocketSynchroniser = new();
|
||||||
|
|
||||||
_LocalPort = localPort;
|
_LocalPort = localPort;
|
||||||
_SocketFactory = socketFactory;
|
_SocketFactory = socketFactory;
|
||||||
|
|
||||||
_RequestParser = new HttpRequestParser();
|
_RequestParser = new();
|
||||||
_ResponseParser = new HttpResponseParser();
|
_ResponseParser = new();
|
||||||
|
|
||||||
_MulticastTtl = multicastTimeToLive;
|
_MulticastTtl = multicastTimeToLive;
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_enableMultiSocketBinding = enableMultiSocketBinding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure
|
||||||
{
|
{
|
||||||
sockets = sockets.ToList();
|
sockets = sockets.ToList();
|
||||||
|
|
||||||
var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address)))
|
var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))
|
||||||
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
|
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
|
||||||
return Task.WhenAll(tasks);
|
return Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
|
@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure
|
||||||
{
|
{
|
||||||
var sockets = new List<Socket>();
|
var sockets = new List<Socket>();
|
||||||
var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress);
|
var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress);
|
||||||
if (_enableMultiSocketBinding)
|
|
||||||
{
|
|
||||||
// IPv6 is currently unsupported
|
|
||||||
var validInterfaces = _networkManager.GetInternalBindAddresses()
|
|
||||||
.Where(x => x.Address is not null)
|
|
||||||
.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
|
|
||||||
.DistinctBy(x => x.Index);
|
|
||||||
|
|
||||||
foreach (var intf in validInterfaces)
|
// IPv6 is currently unsupported
|
||||||
{
|
var validInterfaces = _networkManager.GetInternalBindAddresses()
|
||||||
try
|
.Where(x => x.Address is not null)
|
||||||
{
|
.Where(x => x.SupportsMulticast)
|
||||||
var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
|
.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
|
||||||
_ = ListenToSocketInternal(socket);
|
.DistinctBy(x => x.Index);
|
||||||
sockets.Add(socket);
|
|
||||||
}
|
foreach (var intf in validInterfaces)
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort);
|
try
|
||||||
_ = ListenToSocketInternal(socket);
|
{
|
||||||
sockets.Add(socket);
|
var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
|
||||||
|
_ = ListenToSocketInternal(socket);
|
||||||
|
sockets.Add(socket);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sockets;
|
return sockets;
|
||||||
|
@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure
|
||||||
private List<Socket> CreateSendSockets()
|
private List<Socket> CreateSendSockets()
|
||||||
{
|
{
|
||||||
var sockets = new List<Socket>();
|
var sockets = new List<Socket>();
|
||||||
if (_enableMultiSocketBinding)
|
|
||||||
{
|
|
||||||
// IPv6 is currently unsupported
|
|
||||||
var validInterfaces = _networkManager.GetInternalBindAddresses()
|
|
||||||
.Where(x => x.Address is not null)
|
|
||||||
.Where(x => x.AddressFamily == AddressFamily.InterNetwork);
|
|
||||||
|
|
||||||
foreach (var intf in validInterfaces)
|
// IPv6 is currently unsupported
|
||||||
|
var validInterfaces = _networkManager.GetInternalBindAddresses()
|
||||||
|
.Where(x => x.Address is not null)
|
||||||
|
.Where(x => x.SupportsMulticast)
|
||||||
|
.Where(x => x.AddressFamily == AddressFamily.InterNetwork);
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340
|
||||||
|
validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var intf in validInterfaces)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
|
||||||
{
|
_ = ListenToSocketInternal(socket);
|
||||||
var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
|
sockets.Add(socket);
|
||||||
_ = ListenToSocketInternal(socket);
|
}
|
||||||
sockets.Add(socket);
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
_logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort);
|
|
||||||
_ = ListenToSocketInternal(socket);
|
|
||||||
sockets.Add(socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return sockets;
|
return sockets;
|
||||||
}
|
}
|
||||||
|
@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);
|
var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
if (result.ReceivedBytes > 0)
|
if (result.ReceivedBytes > 0)
|
||||||
{
|
{
|
||||||
|
@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure
|
||||||
var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface);
|
var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface);
|
||||||
|
|
||||||
ProcessMessage(
|
ProcessMessage(
|
||||||
UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
|
Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
|
||||||
remoteEndpoint,
|
remoteEndpoint,
|
||||||
localEndpointAdapter.Address);
|
localEndpointAdapter.Address);
|
||||||
}
|
}
|
||||||
|
@ -511,13 +507,13 @@ namespace Rssdp.Infrastructure
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlers = this.RequestReceived;
|
var handlers = RequestReceived;
|
||||||
handlers?.Invoke(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
|
handlers?.Invoke(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress)
|
private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress)
|
||||||
{
|
{
|
||||||
var handlers = this.ResponseReceived;
|
var handlers = ResponseReceived;
|
||||||
handlers?.Invoke(this, new ResponseReceivedEventArgs(data, endPoint)
|
handlers?.Invoke(this, new ResponseReceivedEventArgs(data, endPoint)
|
||||||
{
|
{
|
||||||
LocalIPAddress = localIPAddress
|
LocalIPAddress = localIPAddress
|
||||||
|
|
|
@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure
|
||||||
private ISsdpCommunicationsServer _CommunicationsServer;
|
private ISsdpCommunicationsServer _CommunicationsServer;
|
||||||
|
|
||||||
private Timer _BroadcastTimer;
|
private Timer _BroadcastTimer;
|
||||||
private object _timerLock = new object();
|
private object _timerLock = new();
|
||||||
|
|
||||||
private string _OSName;
|
private string _OSName;
|
||||||
|
|
||||||
|
@ -221,12 +221,12 @@ namespace Rssdp.Infrastructure
|
||||||
/// <seealso cref="DeviceAvailable"/>
|
/// <seealso cref="DeviceAvailable"/>
|
||||||
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
|
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
|
||||||
{
|
{
|
||||||
if (this.IsDisposed)
|
if (IsDisposed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlers = this.DeviceAvailable;
|
var handlers = DeviceAvailable;
|
||||||
handlers?.Invoke(this, new DeviceAvailableEventArgs(device, isNewDevice)
|
handlers?.Invoke(this, new DeviceAvailableEventArgs(device, isNewDevice)
|
||||||
{
|
{
|
||||||
RemoteIPAddress = IPAddress
|
RemoteIPAddress = IPAddress
|
||||||
|
@ -241,12 +241,12 @@ namespace Rssdp.Infrastructure
|
||||||
/// <seealso cref="DeviceUnavailable"/>
|
/// <seealso cref="DeviceUnavailable"/>
|
||||||
protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired)
|
protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired)
|
||||||
{
|
{
|
||||||
if (this.IsDisposed)
|
if (IsDisposed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlers = this.DeviceUnavailable;
|
var handlers = DeviceUnavailable;
|
||||||
handlers?.Invoke(this, new DeviceUnavailableEventArgs(device, expired));
|
handlers?.Invoke(this, new DeviceUnavailableEventArgs(device, expired));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,8 +285,8 @@ namespace Rssdp.Infrastructure
|
||||||
_CommunicationsServer = null;
|
_CommunicationsServer = null;
|
||||||
if (commsServer is not null)
|
if (commsServer is not null)
|
||||||
{
|
{
|
||||||
commsServer.ResponseReceived -= this.CommsServer_ResponseReceived;
|
commsServer.ResponseReceived -= CommsServer_ResponseReceived;
|
||||||
commsServer.RequestReceived -= this.CommsServer_RequestReceived;
|
commsServer.RequestReceived -= CommsServer_RequestReceived;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,7 +335,7 @@ namespace Rssdp.Infrastructure
|
||||||
|
|
||||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
values["HOST"] = "239.255.255.250:1900";
|
values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort);
|
||||||
values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
|
values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
|
||||||
values["MAN"] = "\"ssdp:discover\"";
|
values["MAN"] = "\"ssdp:discover\"";
|
||||||
|
|
||||||
|
@ -376,17 +376,17 @@ namespace Rssdp.Infrastructure
|
||||||
|
|
||||||
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress)
|
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress)
|
||||||
{
|
{
|
||||||
if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
|
if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationType = GetFirstHeaderStringValue("NTS", message);
|
var notificationType = GetFirstHeaderStringValue("NTS", message);
|
||||||
if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
|
if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
{
|
{
|
||||||
ProcessAliveNotification(message, IPAddress);
|
ProcessAliveNotification(message, IPAddress);
|
||||||
}
|
}
|
||||||
else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
|
else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
{
|
{
|
||||||
ProcessByeByeNotification(message);
|
ProcessByeByeNotification(message);
|
||||||
}
|
}
|
||||||
|
@ -414,7 +414,7 @@ namespace Rssdp.Infrastructure
|
||||||
private void ProcessByeByeNotification(HttpRequestMessage message)
|
private void ProcessByeByeNotification(HttpRequestMessage message)
|
||||||
{
|
{
|
||||||
var notficationType = GetFirstHeaderStringValue("NT", message);
|
var notficationType = GetFirstHeaderStringValue("NT", message);
|
||||||
if (!String.IsNullOrEmpty(notficationType))
|
if (!string.IsNullOrEmpty(notficationType))
|
||||||
{
|
{
|
||||||
var usn = GetFirstHeaderStringValue("USN", message);
|
var usn = GetFirstHeaderStringValue("USN", message);
|
||||||
|
|
||||||
|
@ -519,7 +519,7 @@ namespace Rssdp.Infrastructure
|
||||||
|
|
||||||
foreach (var device in expiredDevices)
|
foreach (var device in expiredDevices)
|
||||||
{
|
{
|
||||||
if (this.IsDisposed)
|
if (IsDisposed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -533,7 +533,7 @@ namespace Rssdp.Infrastructure
|
||||||
// problems.
|
// problems.
|
||||||
foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct())
|
foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct())
|
||||||
{
|
{
|
||||||
if (this.IsDisposed)
|
if (IsDisposed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -550,7 +550,7 @@ namespace Rssdp.Infrastructure
|
||||||
existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn);
|
existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn);
|
||||||
foreach (var existingDevice in existingDevices)
|
foreach (var existingDevice in existingDevices)
|
||||||
{
|
{
|
||||||
if (this.IsDisposed)
|
if (IsDisposed)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure
|
||||||
IPAddress receivedOnlocalIPAddress,
|
IPAddress receivedOnlocalIPAddress,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrEmpty(searchTarget))
|
if (string.IsNullOrEmpty(searchTarget))
|
||||||
{
|
{
|
||||||
WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
|
WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure
|
||||||
// return;
|
// return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
|
if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not block synchronously as that may tie up a threadpool thread for several seconds.
|
// Do not block synchronously as that may tie up a threadpool thread for several seconds.
|
||||||
Task.Delay(_Random.Next(16, (maxWaitInterval * 1000)), cancellationToken).ContinueWith((parentTask) =>
|
Task.Delay(_Random.Next(16, maxWaitInterval * 1000), cancellationToken).ContinueWith((parentTask) =>
|
||||||
{
|
{
|
||||||
// Copying devices to local array here to avoid threading issues/enumerator exceptions.
|
// Copying devices to local array here to avoid threading issues/enumerator exceptions.
|
||||||
IEnumerable<SsdpDevice> devices = null;
|
IEnumerable<SsdpDevice> devices = null;
|
||||||
lock (_Devices)
|
lock (_Devices)
|
||||||
{
|
{
|
||||||
if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
|
if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
{
|
{
|
||||||
devices = GetAllDevicesAsFlatEnumerable().ToArray();
|
devices = GetAllDevicesAsFlatEnumerable().ToArray();
|
||||||
}
|
}
|
||||||
else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
|
else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
|
||||||
{
|
{
|
||||||
devices = _Devices.ToArray();
|
devices = _Devices.ToArray();
|
||||||
}
|
}
|
||||||
else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase))
|
else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
|
devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
|
||||||
}
|
}
|
||||||
else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase))
|
else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
|
devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure
|
||||||
if (isRootDevice)
|
if (isRootDevice)
|
||||||
{
|
{
|
||||||
SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
|
SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
|
||||||
if (this.SupportPnpRootDevice)
|
if (SupportPnpRootDevice)
|
||||||
{
|
{
|
||||||
SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
|
SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure
|
||||||
|
|
||||||
private string GetUsn(string udn, string fullDeviceType)
|
private string GetUsn(string udn, string fullDeviceType)
|
||||||
{
|
{
|
||||||
return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
|
return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SendSearchResponse(
|
private async void SendSearchResponse(
|
||||||
|
@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure
|
||||||
const string header = "HTTP/1.1 200 OK";
|
const string header = "HTTP/1.1 200 OK";
|
||||||
|
|
||||||
var rootDevice = device.ToRootDevice();
|
var rootDevice = device.ToRootDevice();
|
||||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
values["EXT"] = "";
|
["EXT"] = "",
|
||||||
values["DATE"] = DateTime.UtcNow.ToString("r");
|
["DATE"] = DateTime.UtcNow.ToString("r"),
|
||||||
values["HOST"] = "239.255.255.250:1900";
|
["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
|
||||||
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
|
["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
|
||||||
values["ST"] = searchTarget;
|
["ST"] = searchTarget,
|
||||||
values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
|
["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
|
||||||
values["USN"] = uniqueServiceName;
|
["USN"] = uniqueServiceName,
|
||||||
values["LOCATION"] = rootDevice.Location.ToString();
|
["LOCATION"] = rootDevice.Location.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
var message = BuildMessage(header, values);
|
var message = BuildMessage(header, values);
|
||||||
|
|
||||||
|
@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure
|
||||||
if (isRoot)
|
if (isRoot)
|
||||||
{
|
{
|
||||||
SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken);
|
SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken);
|
||||||
if (this.SupportPnpRootDevice)
|
if (SupportPnpRootDevice)
|
||||||
{
|
{
|
||||||
SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken);
|
SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure
|
||||||
|
|
||||||
const string header = "NOTIFY * HTTP/1.1";
|
const string header = "NOTIFY * HTTP/1.1";
|
||||||
|
|
||||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
// If needed later for non-server devices, these headers will need to be dynamic
|
// If needed later for non-server devices, these headers will need to be dynamic
|
||||||
values["HOST"] = "239.255.255.250:1900";
|
["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
|
||||||
values["DATE"] = DateTime.UtcNow.ToString("r");
|
["DATE"] = DateTime.UtcNow.ToString("r"),
|
||||||
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
|
["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
|
||||||
values["LOCATION"] = rootDevice.Location.ToString();
|
["LOCATION"] = rootDevice.Location.ToString(),
|
||||||
values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
|
["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
|
||||||
values["NTS"] = "ssdp:alive";
|
["NTS"] = "ssdp:alive",
|
||||||
values["NT"] = notificationType;
|
["NT"] = notificationType,
|
||||||
values["USN"] = uniqueServiceName;
|
["USN"] = uniqueServiceName
|
||||||
|
};
|
||||||
|
|
||||||
var message = BuildMessage(header, values);
|
var message = BuildMessage(header, values);
|
||||||
|
|
||||||
|
@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure
|
||||||
if (isRoot)
|
if (isRoot)
|
||||||
{
|
{
|
||||||
tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken));
|
tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken));
|
||||||
if (this.SupportPnpRootDevice)
|
if (SupportPnpRootDevice)
|
||||||
{
|
{
|
||||||
tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken));
|
tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken));
|
||||||
}
|
}
|
||||||
|
@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure
|
||||||
{
|
{
|
||||||
const string header = "NOTIFY * HTTP/1.1";
|
const string header = "NOTIFY * HTTP/1.1";
|
||||||
|
|
||||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
// If needed later for non-server devices, these headers will need to be dynamic
|
// If needed later for non-server devices, these headers will need to be dynamic
|
||||||
values["HOST"] = "239.255.255.250:1900";
|
["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
|
||||||
values["DATE"] = DateTime.UtcNow.ToString("r");
|
["DATE"] = DateTime.UtcNow.ToString("r"),
|
||||||
values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
|
["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
|
||||||
values["NTS"] = "ssdp:byebye";
|
["NTS"] = "ssdp:byebye",
|
||||||
values["NT"] = notificationType;
|
["NT"] = notificationType,
|
||||||
values["USN"] = uniqueServiceName;
|
["USN"] = uniqueServiceName
|
||||||
|
};
|
||||||
|
|
||||||
var message = BuildMessage(header, values);
|
var message = BuildMessage(header, values);
|
||||||
|
|
||||||
var sendCount = IsDisposed ? 1 : 3;
|
var sendCount = IsDisposed ? 1 : 3;
|
||||||
WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
|
WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
|
||||||
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
|
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ RUN yum update -yq \
|
||||||
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
|
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
|
||||||
|
|
||||||
# Install DotNET SDK
|
# Install DotNET SDK
|
||||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
|
|
@ -12,7 +12,7 @@ RUN dnf update -yq \
|
||||||
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
|
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
|
||||||
|
|
||||||
# Install DotNET SDK
|
# Install DotNET SDK
|
||||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
|
|
@ -17,7 +17,7 @@ RUN apt-get update -yqq \
|
||||||
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
|
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
|
||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update -yqq \
|
||||||
mmv build-essential lsb-release
|
mmv build-essential lsb-release
|
||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update -yqq \
|
||||||
mmv build-essential lsb-release
|
mmv build-essential lsb-release
|
||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
|
|
@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
|
var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan())));
|
||||||
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
|
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
File.Copy(path, tempPath, true);
|
File.Copy(path, tempPath, true);
|
||||||
|
@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder
|
||||||
{
|
{
|
||||||
if (!orientation.HasValue)
|
if (!orientation.HasValue)
|
||||||
{
|
{
|
||||||
return SKEncodedOrigin.TopLeft;
|
return SKEncodedOrigin.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return orientation.Value switch
|
return (SKEncodedOrigin)orientation.Value;
|
||||||
{
|
|
||||||
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
|
|
||||||
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
|
|
||||||
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
|
|
||||||
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
|
|
||||||
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
|
|
||||||
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
|
|
||||||
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
|
|
||||||
_ => SKEncodedOrigin.TopLeft
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -38,25 +38,25 @@ public partial class StripCollageBuilder
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(outputPath);
|
ArgumentNullException.ThrowIfNull(outputPath);
|
||||||
|
|
||||||
var ext = Path.GetExtension(outputPath);
|
var ext = Path.GetExtension(outputPath.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
|
if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
|| ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Jpeg;
|
return SKEncodedImageFormat.Jpeg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Webp;
|
return SKEncodedImageFormat.Webp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Gif;
|
return SKEncodedImageFormat.Gif;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Bmp;
|
return SKEncodedImageFormat.Bmp;
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
|
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
|
||||||
{
|
|
||||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
|
||||||
using var fileStream = AsyncFile.OpenRead(file.Path);
|
|
||||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
|
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
|
||||||
=> _imageEncoder.SupportedOutputFormats;
|
=> _imageEncoder.SupportedOutputFormats;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool SupportsTransparency(string path)
|
|
||||||
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
||||||
{
|
{
|
||||||
|
@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
|
return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||||
return ImageFormat.Jpg;
|
return ImageFormat.Jpg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMimeType(ImageFormat format, string path)
|
|
||||||
=> format switch
|
|
||||||
{
|
|
||||||
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
|
||||||
ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
|
|
||||||
ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
|
|
||||||
ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
|
|
||||||
ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
|
|
||||||
_ => MimeTypes.GetMimeType(path)
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cache file path based on a set of parameters.
|
/// Gets the cache file path based on a set of parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||||
filename.Append(",v=");
|
filename.Append(",v=");
|
||||||
filename.Append(Version);
|
filename.Append(Version);
|
||||||
|
|
||||||
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||||
return Task.FromResult((originalImagePath, dateModified));
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO _mediaEncoder.ConvertImage is not implemented
|
|
||||||
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
|
||||||
//
|
|
||||||
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
|
||||||
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
|
||||||
//
|
|
||||||
// var file = _fileSystem.GetFileInfo(outputPath);
|
|
||||||
// if (!file.Exists)
|
|
||||||
// {
|
|
||||||
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
|
||||||
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// dateModified = file.LastWriteTimeUtc;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// originalImagePath = outputPath;
|
|
||||||
// }
|
|
||||||
// catch (Exception ex)
|
|
||||||
// {
|
|
||||||
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return Task.FromResult((originalImagePath, dateModified));
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
|
||||||
[InlineData((ImageFormat)5)]
|
[InlineData((ImageFormat)5)]
|
||||||
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
||||||
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
|
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetAllImageFormats))]
|
||||||
|
public static void GetExtension_Valid_Valid(ImageFormat format)
|
||||||
|
=> Assert.Null(Record.Exception(() => format.GetExtension()));
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData((ImageFormat)int.MinValue)]
|
||||||
|
[InlineData((ImageFormat)int.MaxValue)]
|
||||||
|
[InlineData((ImageFormat)(-1))]
|
||||||
|
[InlineData((ImageFormat)5)]
|
||||||
|
public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
||||||
|
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetExtension());
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests
|
||||||
[InlineData("127.0.0.1:123")]
|
[InlineData("127.0.0.1:123")]
|
||||||
[InlineData("localhost")]
|
[InlineData("localhost")]
|
||||||
[InlineData("localhost:1345")]
|
[InlineData("localhost:1345")]
|
||||||
[InlineData("www.google.co.uk")]
|
|
||||||
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
|
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
|
||||||
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
|
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
|
||||||
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
|
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
using Jellyfin.Networking.Manager;
|
using Jellyfin.Networking.Manager;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Jellyfin.Networking.Tests
|
namespace Jellyfin.Networking.Tests
|
||||||
|
@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests
|
||||||
LocalNetworkSubnets = network.Split(',')
|
LocalNetworkSubnets = network.Split(',')
|
||||||
};
|
};
|
||||||
|
|
||||||
using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
|
|
||||||
Assert.True(networkManager.IsInLocalNetwork(ip));
|
Assert.True(networkManager.IsInLocalNetwork(ip));
|
||||||
}
|
}
|
||||||
|
@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests
|
||||||
LocalNetworkSubnets = network.Split(',')
|
LocalNetworkSubnets = network.Split(',')
|
||||||
};
|
};
|
||||||
|
|
||||||
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
|
|
||||||
Assert.False(nm.IsInLocalNetwork(ip));
|
Assert.False(networkManager.IsInLocalNetwork(ip));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions;
|
||||||
using Jellyfin.Networking.Manager;
|
using Jellyfin.Networking.Manager;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkManager.MockNetworkSettings = interfaces;
|
NetworkManager.MockNetworkSettings = interfaces;
|
||||||
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
NetworkManager.MockNetworkSettings = string.Empty;
|
NetworkManager.MockNetworkSettings = string.Empty;
|
||||||
|
|
||||||
Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]");
|
Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]");
|
||||||
|
@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
|
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
|
||||||
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
NetworkManager.MockNetworkSettings = string.Empty;
|
NetworkManager.MockNetworkSettings = string.Empty;
|
||||||
|
|
||||||
// Check to see if DNS resolution is working. If not, skip test.
|
// Check to see if DNS resolution is working. If not, skip test.
|
||||||
|
@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests
|
||||||
[InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
|
[InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
|
||||||
|
|
||||||
// User on external network, we're bound internal and external - so result is override.
|
// User on external network, we're bound internal and external - so result is override.
|
||||||
[InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
|
[InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=http://helloworld.com", "http://helloworld.com")]
|
||||||
|
|
||||||
// User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override.
|
// User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override.
|
||||||
[InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
|
[InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
|
||||||
|
|
||||||
// User on internal network, no binding specified - so result is the 1st internal.
|
// User on internal network, no binding specified - so result is the 1st internal.
|
||||||
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
|
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")]
|
||||||
|
|
||||||
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
|
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
|
||||||
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
|
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")]
|
||||||
|
|
||||||
// User on external network, no binding - so result is the 1st external which is overriden.
|
// User on external network, no binding - so result is the 1st external which is overriden.
|
||||||
[InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
|
[InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")]
|
||||||
|
|
||||||
// User assumed to be internal, no binding - so result is the 1st internal.
|
// User assumed to be internal, no binding - so result is the 1st matching interface.
|
||||||
[InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
|
[InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")]
|
||||||
|
|
||||||
// User is internal, no binding - so result is the 1st internal, which is then overridden.
|
// User is internal, no binding - so result is the 1st internal interface, which is then overridden.
|
||||||
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
|
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
|
||||||
public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
|
public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
|
||||||
{
|
{
|
||||||
|
@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
|
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
|
||||||
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
NetworkManager.MockNetworkSettings = string.Empty;
|
NetworkManager.MockNetworkSettings = string.Empty;
|
||||||
|
|
||||||
if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null)
|
if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null)
|
||||||
|
@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests
|
||||||
RemoteIPFilter = addresses.Split(','),
|
RemoteIPFilter = addresses.Split(','),
|
||||||
IsRemoteIPFilterBlacklist = false
|
IsRemoteIPFilterBlacklist = false
|
||||||
};
|
};
|
||||||
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
|
||||||
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
|
|
||||||
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
|
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
|
||||||
}
|
}
|
||||||
|
@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests
|
||||||
IsRemoteIPFilterBlacklist = true
|
IsRemoteIPFilterBlacklist = true
|
||||||
};
|
};
|
||||||
|
|
||||||
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
|
|
||||||
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
|
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
|
||||||
}
|
}
|
||||||
|
@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkManager.MockNetworkSettings = interfaces;
|
NetworkManager.MockNetworkSettings = interfaces;
|
||||||
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
|
|
||||||
var interfaceToUse = nm.GetBindAddress(string.Empty, out _);
|
var interfaceToUse = nm.GetBindAddress(string.Empty, out _);
|
||||||
|
|
||||||
|
@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkManager.MockNetworkSettings = interfaces;
|
NetworkManager.MockNetworkSettings = interfaces;
|
||||||
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
var startupConf = new Mock<IConfiguration>();
|
||||||
|
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
|
|
||||||
var interfaceToUse = nm.GetBindAddress(source, out _);
|
var interfaceToUse = nm.GetBindAddress(source, out _);
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests
|
||||||
{
|
{
|
||||||
public static class AuthHelper
|
public static class AuthHelper
|
||||||
{
|
{
|
||||||
public const string AuthHeaderName = "X-Emby-Authorization";
|
public const string AuthHeaderName = "Authorization";
|
||||||
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
|
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\"";
|
||||||
|
|
||||||
public static async Task<string> CompleteStartupAsync(HttpClient client)
|
public static async Task<string> CompleteStartupAsync(HttpClient client)
|
||||||
{
|
{
|
||||||
|
@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests
|
||||||
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
|
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
|
||||||
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
|
||||||
|
|
||||||
using var content = JsonContent.Create(
|
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName");
|
||||||
|
httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader);
|
||||||
|
httpRequest.Content = JsonContent.Create(
|
||||||
new AuthenticateUserByName()
|
new AuthenticateUserByName()
|
||||||
{
|
{
|
||||||
Username = user!.Name,
|
Username = user!.Name,
|
||||||
Pw = user.Password,
|
Pw = user.Password,
|
||||||
},
|
},
|
||||||
options: jsonOptions);
|
options: jsonOptions);
|
||||||
content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
|
|
||||||
|
|
||||||
using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content);
|
using var authResponse = await client.SendAsync(httpRequest);
|
||||||
|
authResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
|
var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
|
||||||
await authResponse.Content.ReadAsStreamAsync(),
|
await authResponse.Content.ReadAsStreamAsync(),
|
||||||
jsonOptions);
|
jsonOptions);
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Integration.Tests.Controllers;
|
||||||
|
|
||||||
|
public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory>
|
||||||
|
{
|
||||||
|
private readonly JellyfinApplicationFactory _factory;
|
||||||
|
private static string? _accessToken;
|
||||||
|
|
||||||
|
public PersonsControllerTests(JellyfinApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPerson_DoesntExist_NotFound()
|
||||||
|
{
|
||||||
|
var client = _factory.CreateClient();
|
||||||
|
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||||
|
|
||||||
|
using var response = await client.GetAsync($"Persons/DoesntExist");
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests
|
||||||
EnableIPv6 = true,
|
EnableIPv6 = true,
|
||||||
EnableIPv4 = true,
|
EnableIPv4 = true,
|
||||||
};
|
};
|
||||||
|
var startupConf = new Mock<IConfiguration>();
|
||||||
return new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
|
return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
||||||
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
|
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
|
||||||
|
|
||||||
var item = result.Item;
|
var item = result.Item;
|
||||||
Assert.Equal("Rising (1)", item.Name);
|
Assert.Equal("Rising (1) / Rising (2)", item.Name);
|
||||||
Assert.Equal(1, item.IndexNumber);
|
Assert.Equal(1, item.IndexNumber);
|
||||||
Assert.Equal(2, item.IndexNumberEnd);
|
Assert.Equal(2, item.IndexNumberEnd);
|
||||||
Assert.Equal(1, item.ParentIndexNumber);
|
Assert.Equal(1, item.ParentIndexNumber);
|
||||||
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
|
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview);
|
||||||
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
|
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
|
||||||
Assert.Equal(2004, item.ProductionYear);
|
Assert.Equal(2004, item.ProductionYear);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user