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:
Stepan Goremykin 2023-10-12 20:11:16 +02:00
commit 8d7e4229ca
83 changed files with 1484 additions and 2131 deletions

View File

@ -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

View File

@ -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

View File

@ -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" />

View File

@ -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

View File

@ -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");
} }
} }

View File

@ -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)))
{ {

View File

@ -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;

View File

@ -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
{ {

View File

@ -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);

View File

@ -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);
} }
} }

View File

@ -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)
{ {

View File

@ -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))

View File

@ -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;
} }

View File

@ -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>

View File

@ -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

View File

@ -121,5 +121,7 @@
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ", "HearingImpaired": "കേൾവി തകരാറുകൾ",
"External": "പുറമേയുള്ള" "External": "പുറമേയുള്ള",
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
} }

View File

@ -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í"
} }

View File

@ -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)

View File

@ -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;
} }

View File

@ -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())

View 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();
});
}
}

View File

@ -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");
} }
} }
} }

View File

@ -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;

View File

@ -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);
} }

View File

@ -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));

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

View File

@ -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);

View File

@ -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);
} }

View File

@ -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;
}
} }

View File

@ -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.

View File

@ -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);

View File

@ -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));

View File

@ -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)))

View File

@ -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));
}
}
} }
} }

View File

@ -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)
{ {

View File

@ -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)
{ {

View File

@ -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)

View File

@ -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;
} }
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -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);
} }
} }

View File

@ -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));
} }
} }
} }

View 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 });
}

View File

@ -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>

View 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();
}

View File

@ -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

View File

@ -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);
}
} }
} }

View File

@ -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

View File

@ -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;
} }
} }
} }

View File

@ -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()

View File

@ -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;

View File

@ -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))
};
} }

View File

@ -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>

View 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; }
}

View File

@ -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.

View File

@ -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))
{ {

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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");
} }
} }

View File

@ -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

View File

@ -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;
} }

View File

@ -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);
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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;
} }

View File

@ -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));
} }

View File

@ -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());
} }

View File

@ -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")]

View File

@ -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));
} }
} }
} }

View File

@ -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 _);

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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>());
} }
} }
} }

View File

@ -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);
} }