Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aefb9b2cff | |||
| abc51e89a3 | |||
| c929229030 | |||
| f625665cb1 | |||
| 79f3ce5325 | |||
| 6938fac73e | |||
| cafa0a1e10 | |||
| 24242a510d | |||
| 2d95245405 | |||
| 784f6c5eff | |||
| 3ee7194706 | |||
| a4ab5e5a14 | |||
| 1fff472569 | |||
| 91ca81eca7 | |||
| 78d9fa72e0 | |||
| 9eb2044eae | |||
| 7ea09a8637 | |||
| 069b518ab0 | |||
| ae6a7acf14 | |||
| 64e0f9099a | |||
| 4f94d23011 | |||
| fcdef875a2 | |||
| 2da4a2d753 | |||
| 9ae68057a7 | |||
| 767a5e6193 | |||
| 346f36bc09 | |||
| 1daf761aec | |||
| a70a09fdb3 | |||
| 888838adbc | |||
| 72911501d3 | |||
| 1af7b6d348 | |||
| 317d7a9f4f | |||
| 9d9c6fe5eb | |||
| a5b771861f | |||
| 3f539472f3 | |||
| 7f43521b64 | |||
| 99006c370f | |||
| e3f9f0a7f3 | |||
| d1fbdcee34 | |||
| 21e398ba0c | |||
| 8544e7fc72 | |||
| 117d2082aa | |||
| 03082e90f9 | |||
| 88026518b1 | |||
| 5f1fb26382 | |||
| 070d04c1b2 | |||
| 8aa4e2e320 | |||
| 49bb5a6442 | |||
| 9e869b4541 | |||
| 710e877762 | |||
| f536e08e14 | |||
| 4eecfee29f | |||
| 731874429c | |||
| e6c6441abf | |||
| 4d89a095ed | |||
| ba28e3041d | |||
| 34f3ed0a4d |
@@ -22,7 +22,7 @@
|
|||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.8" />
|
<PackageVersion Include="libse" Version="4.0.8" />
|
||||||
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
<PackageVersion Include="LrcParser" Version="2025.228.1" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageVersion Include="z440.atl.core" Version="6.11.0" />
|
<PackageVersion Include="z440.atl.core" Version="6.20.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.10.5</VersionPrefix>
|
<VersionPrefix>10.10.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -454,6 +454,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
_itemRepository.DeleteItem(child.Id);
|
_itemRepository.DeleteItem(child.Id);
|
||||||
|
_cache.TryRemove(child.Id, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cache.TryRemove(item.Id, out _);
|
_cache.TryRemove(item.Id, out _);
|
||||||
@@ -2626,15 +2627,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
episode.ParentIndexNumber = season.IndexNumber;
|
episode.ParentIndexNumber = season.IndexNumber;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
Anime series don't generally have a season in their file name, however,
|
|
||||||
TVDb needs a season to correctly get the metadata.
|
|
||||||
Hence, a null season needs to be filled with something. */
|
|
||||||
// FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
|
|
||||||
episode.ParentIndexNumber = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (episode.ParentIndexNumber.HasValue)
|
if (episode.ParentIndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fairly common for some users to have "Rated R" in their rating field
|
// Fairly common for some users to have "Rated R" in their rating field
|
||||||
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
|
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
|
.Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
// Use rating system matching the language
|
// Use rating system matching the language
|
||||||
if (!string.IsNullOrEmpty(countryCode))
|
if (!string.IsNullOrEmpty(countryCode))
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ TV-PG-DS,10
|
|||||||
TV-PG-DV,10
|
TV-PG-DV,10
|
||||||
TV-PG-LS,10
|
TV-PG-LS,10
|
||||||
TV-PG-LV,10
|
TV-PG-LV,10
|
||||||
TV-PG-SV,1
|
TV-PG-SV,10
|
||||||
TV-PG-DLS,10
|
TV-PG-DLS,10
|
||||||
TV-PG-DLV,10
|
TV-PG-DLV,10
|
||||||
TV-PG-DSV,10
|
TV-PG-DSV,10
|
||||||
|
|||||||
|
@@ -116,6 +116,7 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
a.LUFS = await CalculateLUFSAsync(
|
a.LUFS = await CalculateLUFSAsync(
|
||||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||||
|
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -142,7 +143,10 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false);
|
t.LUFS = await CalculateLUFSAsync(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
|
||||||
|
false,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(tracks, cancellationToken);
|
_itemRepository.SaveItems(tracks, cancellationToken);
|
||||||
@@ -162,7 +166,7 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
|
private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
|
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
|
||||||
|
|
||||||
@@ -189,18 +193,28 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
using var reader = process.StandardError;
|
using var reader = process.StandardError;
|
||||||
|
float? lufs = null;
|
||||||
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
|
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
Match match = LUFSRegex().Match(line);
|
Match match = LUFSRegex().Match(line);
|
||||||
|
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogError("Failed to find LUFS value in output");
|
if (lufs is null)
|
||||||
return null;
|
{
|
||||||
|
_logger.LogError("Failed to find LUFS value in output");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitForExit)
|
||||||
|
{
|
||||||
|
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lufs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
private readonly SessionInfo _session;
|
private readonly SessionInfo _session;
|
||||||
|
|
||||||
private readonly List<IWebSocketConnection> _sockets;
|
private readonly List<IWebSocketConnection> _sockets;
|
||||||
|
private readonly ReaderWriterLockSlim _socketsLock;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
public WebSocketController(
|
public WebSocketController(
|
||||||
@@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_session = session;
|
_session = session;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_sockets = new List<IWebSocketConnection>();
|
_sockets = new();
|
||||||
|
_socketsLock = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasOpenSockets => GetActiveSockets().Any();
|
private bool HasOpenSockets
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_socketsLock.EnterReadLock();
|
||||||
|
return _sockets.Any(i => i.State == WebSocketState.Open);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool SupportsMediaControl => HasOpenSockets;
|
public bool SupportsMediaControl => HasOpenSockets;
|
||||||
@@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsSessionActive => HasOpenSockets;
|
public bool IsSessionActive => HasOpenSockets;
|
||||||
|
|
||||||
private IEnumerable<IWebSocketConnection> GetActiveSockets()
|
|
||||||
=> _sockets.Where(i => i.State == WebSocketState.Open);
|
|
||||||
|
|
||||||
public void AddWebSocket(IWebSocketConnection connection)
|
public void AddWebSocket(IWebSocketConnection connection)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
|
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
|
||||||
_sockets.Add(connection);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
try
|
||||||
connection.Closed += OnConnectionClosed;
|
{
|
||||||
|
_socketsLock.EnterWriteLock();
|
||||||
|
_sockets.Add(connection);
|
||||||
|
connection.Closed += OnConnectionClosed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnConnectionClosed(object? sender, EventArgs e)
|
private async void OnConnectionClosed(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
||||||
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||||
_sockets.Remove(connection);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
connection.Closed -= OnConnectionClosed;
|
try
|
||||||
|
{
|
||||||
|
_socketsLock.EnterWriteLock();
|
||||||
|
_sockets.Remove(connection);
|
||||||
|
connection.Closed -= OnConnectionClosed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session
|
|||||||
T data,
|
T data,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
IWebSocketConnection? socket;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_socketsLock.EnterReadLock();
|
||||||
|
socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (socket is null)
|
if (socket is null)
|
||||||
{
|
{
|
||||||
@@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var socket in _sockets)
|
try
|
||||||
{
|
{
|
||||||
socket.Closed -= OnConnectionClosed;
|
_socketsLock.EnterWriteLock();
|
||||||
socket.Dispose();
|
foreach (var socket in _sockets)
|
||||||
|
{
|
||||||
|
socket.Closed -= OnConnectionClosed;
|
||||||
|
socket.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_sockets.Clear();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_socketsLock.Dispose();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var socket in _sockets)
|
try
|
||||||
{
|
{
|
||||||
socket.Closed -= OnConnectionClosed;
|
_socketsLock.EnterWriteLock();
|
||||||
await socket.DisposeAsync().ConfigureAwait(false);
|
foreach (var socket in _sockets)
|
||||||
|
{
|
||||||
|
socket.Closed -= OnConnectionClosed;
|
||||||
|
await socket.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sockets.Clear();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_socketsLock.Dispose();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[ProducesAudioFile]
|
[ProducesAudioFile]
|
||||||
public async Task<ActionResult> GetAudioStream(
|
public async Task<ActionResult> GetAudioStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[ProducesAudioFile]
|
[ProducesAudioFile]
|
||||||
public async Task<ActionResult> GetAudioStreamByContainer(
|
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
|||||||
@@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[ProducesPlaylistFile]
|
[ProducesPlaylistFile]
|
||||||
public async Task<ActionResult> GetLiveHlsStream(
|
public async Task<ActionResult> GetLiveHlsStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -415,12 +415,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery, Required] string mediaSourceId,
|
[FromQuery, Required] string mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -431,7 +431,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -452,8 +452,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -591,12 +591,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery, Required] string mediaSourceId,
|
[FromQuery, Required] string mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -608,7 +608,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -627,8 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -761,12 +761,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -777,7 +777,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -798,8 +798,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -933,12 +933,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -950,7 +950,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -969,8 +969,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1106,7 +1106,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery, Required] long runtimeTicks,
|
[FromQuery, Required] long runtimeTicks,
|
||||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
@@ -1114,12 +1114,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -1130,7 +1130,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -1151,8 +1151,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1291,7 +1291,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery, Required] long runtimeTicks,
|
[FromQuery, Required] long runtimeTicks,
|
||||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
@@ -1299,12 +1299,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -1316,7 +1316,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -1335,8 +1335,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
|||||||
@@ -1186,7 +1186,9 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
|
public ActionResult GetLiveStreamFile(
|
||||||
|
[FromRoute, Required] string streamId,
|
||||||
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
|
||||||
{
|
{
|
||||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
||||||
if (liveStreamInfo is null)
|
if (liveStreamInfo is null)
|
||||||
|
|||||||
@@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] int? transcodingAudioChannels,
|
[FromQuery] int? transcodingAudioChannels,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery] int? maxStreamingBitrate,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
[FromQuery] long? startTimeTicks,
|
[FromQuery] long? startTimeTicks,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
|
||||||
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
||||||
[FromQuery] int? maxAudioSampleRate,
|
[FromQuery] int? maxAudioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
|
|||||||
@@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public async Task<ActionResult> GetVideoStream(
|
public async Task<ActionResult> GetVideoStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public Task<ActionResult> GetVideoStreamByContainer(
|
public Task<ActionResult> GetVideoStreamByContainer(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] string? deviceProfileId,
|
[FromQuery] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
|||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
protected override void Start(WebSocketMessageInfo message)
|
protected override void Start(WebSocketMessageInfo message)
|
||||||
{
|
{
|
||||||
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) && !message.Connection.AuthorizationInfo.IsApiKey)
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Only admin users can retrieve the activity log.");
|
throw new AuthenticationException("Only admin users can retrieve the activity log.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
|||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
protected override void Start(WebSocketMessageInfo message)
|
protected override void Start(WebSocketMessageInfo message)
|
||||||
{
|
{
|
||||||
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) && !message.Connection.AuthorizationInfo.IsApiKey)
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Only admin users can subscribe to session information.");
|
throw new AuthenticationException("Only admin users can subscribe to session information.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.10.5</VersionPrefix>
|
<VersionPrefix>10.10.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -118,15 +118,15 @@ namespace Jellyfin.Server.Extensions
|
|||||||
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
||||||
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
||||||
|
|
||||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
|
||||||
|
|
||||||
if (config.KnownProxies.Length == 0)
|
if (config.KnownProxies.Length == 0)
|
||||||
{
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.None;
|
||||||
options.KnownNetworks.Clear();
|
options.KnownNetworks.Clear();
|
||||||
options.KnownProxies.Clear();
|
options.KnownProxies.Clear();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
||||||
AddProxyAddresses(config, config.KnownProxies, options);
|
AddProxyAddresses(config, config.KnownProxies, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.10.5</VersionPrefix>
|
<VersionPrefix>10.10.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Channels
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public override SourceType SourceType => SourceType.Channel;
|
public override SourceType SourceType => SourceType.Channel;
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
|
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
|
||||||
if (blockedChannelsPreference.Length != 0)
|
if (blockedChannelsPreference.Length != 0)
|
||||||
@@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Channels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||||
|
|||||||
@@ -1299,7 +1299,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GetParents().Any(i => !i.IsVisible(user)))
|
if (GetParents().Any(i => !i.IsVisible(user, true)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1521,13 +1521,14 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// Determines if a given user has access to this item.
|
/// Determines if a given user has access to this item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
|
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
|
||||||
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
|
||||||
/// <exception cref="ArgumentNullException">If user is null.</exception>
|
/// <exception cref="ArgumentNullException">If user is null.</exception>
|
||||||
public bool IsParentalAllowed(User user)
|
public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
if (!IsVisibleViaTags(user))
|
if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1599,7 +1600,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsVisibleViaTags(User user)
|
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
|
||||||
{
|
{
|
||||||
var allTags = GetInheritedTags();
|
var allTags = GetInheritedTags();
|
||||||
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
||||||
@@ -1614,7 +1615,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
|
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
|
||||||
if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1654,13 +1655,14 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// Default is just parental allowed. Can be overridden for more functionality.
|
/// Default is just parental allowed. Can be overridden for more functionality.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
|
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
|
||||||
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
|
||||||
public virtual bool IsVisible(User user)
|
public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
return IsParentalAllowed(user);
|
return IsParentalAllowed(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool IsVisibleStandalone(User user)
|
public virtual bool IsVisibleStandalone(User user)
|
||||||
|
|||||||
@@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return GetLibraryOptions(Path);
|
return GetLibraryOptions(Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (GetLibraryOptions().Enabled)
|
if (GetLibraryOptions().Enabled)
|
||||||
{
|
{
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
LibraryManager.CreateItem(item, this);
|
LibraryManager.CreateItem(item, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (this is ICollectionFolder && this is not BasePluginFolder)
|
if (this is ICollectionFolder && this is not BasePluginFolder)
|
||||||
{
|
{
|
||||||
@@ -239,7 +239,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1200,6 +1200,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.Is4K.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (request.IsHD.HasValue)
|
if (request.IsHD.HasValue)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -144,14 +144,14 @@ namespace MediaBrowser.Controller.Entities.Movies
|
|||||||
return GetItemLookupInfo<BoxSetInfo>();
|
return GetItemLookupInfo<BoxSetInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (IsLegacyBoxSet)
|
if (IsLegacyBoxSet)
|
||||||
{
|
{
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (base.IsVisible(user))
|
if (base.IsVisible(user, skipAllowedTagsCheck))
|
||||||
{
|
{
|
||||||
if (LinkedChildren.Length == 0)
|
if (LinkedChildren.Length == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -152,16 +152,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
return GetMediaSourceCount(new HashSet<Guid>());
|
||||||
{
|
|
||||||
var item = LibraryManager.GetItemById(PrimaryVersionId);
|
|
||||||
if (item is Video video)
|
|
||||||
{
|
|
||||||
return video.MediaSourceCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,5 +541,25 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int GetMediaSourceCount(HashSet<Guid> callstack)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
||||||
|
{
|
||||||
|
var item = LibraryManager.GetItemById(PrimaryVersionId);
|
||||||
|
if (item is Video video)
|
||||||
|
{
|
||||||
|
if (callstack.Contains(video.Id))
|
||||||
|
{
|
||||||
|
return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
callstack.Add(video.Id);
|
||||||
|
return video.GetMediaSourceCount(callstack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.10.5</VersionPrefix>
|
<VersionPrefix>10.10.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -35,7 +35,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
||||||
/// This should matches all common valid codecs.
|
/// This should matches all common valid codecs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The level validation regex.
|
||||||
|
/// This regular expression matches strings representing a double.
|
||||||
|
/// </summary>
|
||||||
|
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
|
||||||
|
|
||||||
private const string _defaultMjpegEncoder = "mjpeg";
|
private const string _defaultMjpegEncoder = "mjpeg";
|
||||||
|
|
||||||
@@ -75,7 +81,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
||||||
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
||||||
|
|
||||||
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
|
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly string[] _videoProfilesH264 =
|
private static readonly string[] _videoProfilesH264 =
|
||||||
[
|
[
|
||||||
@@ -450,7 +456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return GetMjpegEncoder(state, encodingOptions);
|
return GetMjpegEncoder(state, encodingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_validationRegex.IsMatch(codec))
|
if (_containerValidationRegex.IsMatch(codec))
|
||||||
{
|
{
|
||||||
return codec.ToLowerInvariant();
|
return codec.ToLowerInvariant();
|
||||||
}
|
}
|
||||||
@@ -491,7 +497,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
public static string GetInputFormat(string container)
|
public static string GetInputFormat(string container)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
|
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -709,7 +715,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
var codec = state.OutputAudioCodec;
|
var codec = state.OutputAudioCodec;
|
||||||
|
|
||||||
if (!_validationRegex.IsMatch(codec))
|
if (!_containerValidationRegex.IsMatch(codec))
|
||||||
{
|
{
|
||||||
codec = "aac";
|
codec = "aac";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,11 +227,11 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
return [item];
|
return [item];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (!IsSharedItem)
|
if (!IsSharedItem)
|
||||||
{
|
{
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OpenAccess)
|
if (OpenAccess)
|
||||||
|
|||||||
@@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
|
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
|
||||||
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
|
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
|
||||||
|
|
||||||
var semaphoreCount = 2 * Environment.ProcessorCount;
|
// Although the type is not nullable, this might still be null during unit tests
|
||||||
|
var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
|
||||||
|
if (semaphoreCount < 1)
|
||||||
|
{
|
||||||
|
semaphoreCount = Environment.ProcessorCount;
|
||||||
|
}
|
||||||
|
|
||||||
_thumbnailResourcePool = new(semaphoreCount);
|
_thumbnailResourcePool = new(semaphoreCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
@@ -586,6 +587,7 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the type of the media.
|
/// Gets or sets the type of the media.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The type of the media.</value>
|
/// <value>The type of the media.</value>
|
||||||
|
[DefaultValue(MediaType.Unknown)]
|
||||||
public MediaType MediaType { get; set; }
|
public MediaType MediaType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -34,6 +35,7 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the type.
|
/// Gets or sets the type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The type.</value>
|
/// <value>The type.</value>
|
||||||
|
[DefaultValue(PersonKind.Unknown)]
|
||||||
public PersonKind Type { get; set; }
|
public PersonKind Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// Gets the video range.
|
/// Gets the video range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The video range.</value>
|
/// <value>The video range.</value>
|
||||||
|
[DefaultValue(VideoRange.Unknown)]
|
||||||
public VideoRange VideoRange
|
public VideoRange VideoRange
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -171,6 +172,7 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// Gets the video range type.
|
/// Gets the video range type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The video range type.</value>
|
/// <value>The video range type.</value>
|
||||||
|
[DefaultValue(VideoRangeType.Unknown)]
|
||||||
public VideoRangeType VideoRangeType
|
public VideoRangeType VideoRangeType
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.10.5</VersionPrefix>
|
<VersionPrefix>10.10.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.MediaSegments;
|
namespace MediaBrowser.Model.MediaSegments;
|
||||||
@@ -21,6 +22,7 @@ public class MediaSegmentDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of content this segment defines.
|
/// Gets or sets the type of content this segment defines.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[DefaultValue(MediaSegmentType.Unknown)]
|
||||||
public MediaSegmentType Type { get; set; }
|
public MediaSegmentType Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Search
|
namespace MediaBrowser.Model.Search
|
||||||
@@ -115,6 +116,7 @@ namespace MediaBrowser.Model.Search
|
|||||||
/// Gets or sets the type of the media.
|
/// Gets or sets the type of the media.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The type of the media.</value>
|
/// <value>The type of the media.</value>
|
||||||
|
[DefaultValue(MediaType.Unknown)]
|
||||||
public MediaType MediaType { get; set; }
|
public MediaType MediaType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -551,10 +552,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await using (stream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
var mimetype = response.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
|
||||||
|
}
|
||||||
|
|
||||||
await _providerManager.SaveImage(
|
await _providerManager.SaveImage(
|
||||||
item,
|
item,
|
||||||
stream,
|
stream,
|
||||||
response.Content.Headers.ContentType?.MediaType,
|
mimetype,
|
||||||
type,
|
type,
|
||||||
null,
|
null,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
@@ -677,10 +684,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await using (stream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
var mimetype = response.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
|
||||||
|
}
|
||||||
|
|
||||||
await _providerManager.SaveImage(
|
await _providerManager.SaveImage(
|
||||||
item,
|
item,
|
||||||
stream,
|
stream,
|
||||||
response.Content.Headers.ContentType?.MediaType,
|
mimetype,
|
||||||
imageType,
|
imageType,
|
||||||
null,
|
null,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1010,7 +1010,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaceData || !target.PremiereDate.HasValue)
|
if (replaceData || !target.PremiereDate.HasValue || (IsYearOnlyDate(target.PremiereDate.Value) && source.PremiereDate.HasValue))
|
||||||
{
|
{
|
||||||
target.PremiereDate = source.PremiereDate;
|
target.PremiereDate = source.PremiereDate;
|
||||||
}
|
}
|
||||||
@@ -1142,6 +1142,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsYearOnlyDate(DateTime date) => date.Month == 1 && date.Day == 1;
|
||||||
|
|
||||||
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
|
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
|
||||||
{
|
{
|
||||||
if (target is null)
|
if (target is null)
|
||||||
@@ -1149,13 +1151,24 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
target = new List<PersonInfo>();
|
target = new List<PersonInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var person in target)
|
var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
|
||||||
{
|
var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
|
||||||
var normalizedName = person.Name.RemoveDiacritics();
|
|
||||||
var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (personInSource is not null)
|
foreach (var name in targetByName.Select(g => g.Key))
|
||||||
|
{
|
||||||
|
var targetPeople = targetByName[name].ToArray();
|
||||||
|
var sourcePeople = sourceByName[name].ToArray();
|
||||||
|
|
||||||
|
if (sourcePeople.Length == 0)
|
||||||
{
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < targetPeople.Length; i++)
|
||||||
|
{
|
||||||
|
var person = targetPeople[i];
|
||||||
|
var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
|
||||||
|
|
||||||
foreach (var providerId in personInSource.ProviderIds)
|
foreach (var providerId in personInSource.ProviderIds)
|
||||||
{
|
{
|
||||||
person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
|
person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
|
||||||
@@ -1165,6 +1178,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
person.ImageUrl = personInSource.ImageUrl;
|
person.ImageUrl = personInSource.ImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
|
||||||
|
{
|
||||||
|
person.Role = personInSource.Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
|
||||||
|
{
|
||||||
|
person.SortOrder = personInSource.SortOrder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,20 +204,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
contentType = MediaTypeNames.Image.Png;
|
contentType = MediaTypeNames.Image.Png;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TVDb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
|
// some providers don't correctly report media type, extract from url if no extension found
|
||||||
if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
|
if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
// some iptv/epg providers don't correctly report media type, extract from url if no extension found
|
|
||||||
if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType)))
|
|
||||||
{
|
{
|
||||||
// Strip query parameters from url to get actual path.
|
// Strip query parameters from url to get actual path.
|
||||||
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
|
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
|
||||||
@@ -225,7 +215,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new HttpRequestException($"Request returned {contentType} instead of an image type", null, HttpStatusCode.NotFound);
|
throw new HttpRequestException($"Request returned '{contentType}' instead of an image type", null, HttpStatusCode.NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -170,11 +170,15 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
|
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
// We should never use the property setter of the ATL.Track class.
|
||||||
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
// That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
|
||||||
track.Year ??= mediaInfo.ProductionYear;
|
// For example, setting the Year property will also set the Date property, which is not what we want here.
|
||||||
track.TrackNumber ??= mediaInfo.IndexNumber;
|
// To properly handle fallback values, we make a clone of those fields when valid.
|
||||||
track.DiscNumber ??= mediaInfo.ParentIndexNumber;
|
var trackTitle = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
||||||
|
var trackAlbum = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
||||||
|
var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
|
||||||
|
var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
|
||||||
|
var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
|
||||||
|
|
||||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||||
{
|
{
|
||||||
@@ -271,22 +275,22 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
|
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle))
|
||||||
{
|
{
|
||||||
audio.Name = track.Title;
|
audio.Name = trackTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata)
|
if (options.ReplaceAllMetadata)
|
||||||
{
|
{
|
||||||
audio.Album = track.Album;
|
audio.Album = trackAlbum;
|
||||||
audio.IndexNumber = track.TrackNumber;
|
audio.IndexNumber = trackTrackNumber;
|
||||||
audio.ParentIndexNumber = track.DiscNumber;
|
audio.ParentIndexNumber = trackDiscNumber;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
audio.Album ??= track.Album;
|
audio.Album ??= trackAlbum;
|
||||||
audio.IndexNumber ??= track.TrackNumber;
|
audio.IndexNumber ??= trackTrackNumber;
|
||||||
audio.ParentIndexNumber ??= track.DiscNumber;
|
audio.ParentIndexNumber ??= trackDiscNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.Date.HasValue)
|
if (track.Date.HasValue)
|
||||||
@@ -294,11 +298,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
audio.PremiereDate = track.Date;
|
audio.PremiereDate = track.Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.Year.HasValue)
|
if (trackYear.HasValue)
|
||||||
{
|
{
|
||||||
var year = track.Year.Value;
|
var year = trackYear.Value;
|
||||||
audio.ProductionYear = year;
|
audio.ProductionYear = year;
|
||||||
|
|
||||||
|
// ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks.
|
||||||
if (!audio.PremiereDate.HasValue)
|
if (!audio.PremiereDate.HasValue)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -307,7 +312,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
catch (ArgumentOutOfRangeException ex)
|
catch (ArgumentOutOfRangeException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
|
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,13 +55,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||||||
|
|
||||||
if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
|
if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
|
||||||
&& !string.IsNullOrEmpty(seriesImdbId)
|
&& !string.IsNullOrEmpty(seriesImdbId)
|
||||||
&& info.IndexNumber.HasValue
|
&& info.IndexNumber.HasValue)
|
||||||
&& info.ParentIndexNumber.HasValue)
|
|
||||||
{
|
{
|
||||||
result.HasMetadata = await _omdbProvider.FetchEpisodeData(
|
result.HasMetadata = await _omdbProvider.FetchEpisodeData(
|
||||||
result,
|
result,
|
||||||
info.IndexNumber.Value,
|
info.IndexNumber.Value,
|
||||||
info.ParentIndexNumber.Value,
|
info.ParentIndexNumber ?? 1,
|
||||||
info.GetProviderId(MetadataProvider.Imdb),
|
info.GetProviderId(MetadataProvider.Imdb),
|
||||||
seriesImdbId,
|
seriesImdbId,
|
||||||
info.MetadataLanguage,
|
info.MetadataLanguage,
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
return Enumerable.Empty<RemoteImageInfo>();
|
return Enumerable.Empty<RemoteImageInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var seasonNumber = episode.ParentIndexNumber;
|
var seasonNumber = episode.ParentIndexNumber ?? 1;
|
||||||
var episodeNumber = episode.IndexNumber;
|
var episodeNumber = episode.IndexNumber;
|
||||||
|
|
||||||
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
|
if (!episodeNumber.HasValue)
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<RemoteImageInfo>();
|
return Enumerable.Empty<RemoteImageInfo>();
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
|
|
||||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||||
var episodeResult = await _tmdbClientManager
|
var episodeResult = await _tmdbClientManager
|
||||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
|
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
var stills = episodeResult?.Images?.Stills;
|
var stills = episodeResult?.Images?.Stills;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// The search query must either provide an episode number or date
|
// The search query must either provide an episode number or date
|
||||||
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
|
if (!searchInfo.IndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<RemoteSearchResult>();
|
return Enumerable.Empty<RemoteSearchResult>();
|
||||||
}
|
}
|
||||||
@@ -96,10 +96,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
return metadataResult;
|
return metadataResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seasonNumber = info.ParentIndexNumber;
|
var seasonNumber = info.ParentIndexNumber ?? 1;
|
||||||
var episodeNumber = info.IndexNumber;
|
var episodeNumber = info.IndexNumber;
|
||||||
|
|
||||||
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
|
if (!episodeNumber.HasValue)
|
||||||
{
|
{
|
||||||
return metadataResult;
|
return metadataResult;
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
List<TvEpisode>? result = null;
|
List<TvEpisode>? result = null;
|
||||||
for (int? episode = startindex; episode <= endindex; episode++)
|
for (int? episode = startindex; episode <= endindex; episode++)
|
||||||
{
|
{
|
||||||
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
|
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
|
||||||
if (episodeInfo is not null)
|
if (episodeInfo is not null)
|
||||||
{
|
{
|
||||||
(result ??= new List<TvEpisode>()).Add(episodeInfo);
|
(result ??= new List<TvEpisode>()).Add(episodeInfo);
|
||||||
@@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
episodeResult = await _tmdbClientManager
|
episodeResult = await _tmdbClientManager
|
||||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.10.5")]
|
[assembly: AssemblyVersion("10.10.7")]
|
||||||
[assembly: AssemblyFileVersion("10.10.5")]
|
[assembly: AssemblyFileVersion("10.10.7")]
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
|
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
|
||||||
if (semaphoreCount < 1)
|
if (semaphoreCount < 1)
|
||||||
{
|
{
|
||||||
semaphoreCount = 2 * Environment.ProcessorCount;
|
semaphoreCount = Environment.ProcessorCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
_parallelEncodingLimit = new(semaphoreCount);
|
_parallelEncodingLimit = new(semaphoreCount);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Extensions</PackageId>
|
<PackageId>Jellyfin.Extensions</PackageId>
|
||||||
<VersionPrefix>10.10.5</VersionPrefix>
|
<VersionPrefix>10.10.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities.Libraries;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.LiveTv.Configuration;
|
using Jellyfin.LiveTv.Configuration;
|
||||||
@@ -210,7 +209,7 @@ public class GuideManager : IGuideManager
|
|||||||
progress.Report(15);
|
progress.Report(15);
|
||||||
|
|
||||||
numComplete = 0;
|
numComplete = 0;
|
||||||
var programs = new List<LiveTvProgram>();
|
var programIds = new List<Guid>();
|
||||||
var channels = new List<Guid>();
|
var channels = new List<Guid>();
|
||||||
|
|
||||||
var guideDays = GetGuideDays();
|
var guideDays = GetGuideDays();
|
||||||
@@ -243,8 +242,8 @@ public class GuideManager : IGuideManager
|
|||||||
DtoOptions = new DtoOptions(true)
|
DtoOptions = new DtoOptions(true)
|
||||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||||
|
|
||||||
var newPrograms = new List<Guid>();
|
var newPrograms = new List<LiveTvProgram>();
|
||||||
var updatedPrograms = new List<Guid>();
|
var updatedPrograms = new List<LiveTvProgram>();
|
||||||
|
|
||||||
foreach (var program in channelPrograms)
|
foreach (var program in channelPrograms)
|
||||||
{
|
{
|
||||||
@@ -252,14 +251,14 @@ public class GuideManager : IGuideManager
|
|||||||
var id = programItem.Id;
|
var id = programItem.Id;
|
||||||
if (isNew)
|
if (isNew)
|
||||||
{
|
{
|
||||||
newPrograms.Add(id);
|
newPrograms.Add(programItem);
|
||||||
}
|
}
|
||||||
else if (isUpdated)
|
else if (isUpdated)
|
||||||
{
|
{
|
||||||
updatedPrograms.Add(id);
|
updatedPrograms.Add(programItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
programs.Add(programItem);
|
programIds.Add(programItem.Id);
|
||||||
|
|
||||||
isMovie |= program.IsMovie;
|
isMovie |= program.IsMovie;
|
||||||
isSeries |= program.IsSeries;
|
isSeries |= program.IsSeries;
|
||||||
@@ -276,21 +275,21 @@ public class GuideManager : IGuideManager
|
|||||||
|
|
||||||
if (newPrograms.Count > 0)
|
if (newPrograms.Count > 0)
|
||||||
{
|
{
|
||||||
var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
|
_libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken);
|
||||||
_libraryManager.CreateItems(newProgramDtos, null, cancellationToken);
|
|
||||||
|
await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedPrograms.Count > 0)
|
if (updatedPrograms.Count > 0)
|
||||||
{
|
{
|
||||||
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
|
|
||||||
await _libraryManager.UpdateItemsAsync(
|
await _libraryManager.UpdateItemsAsync(
|
||||||
updatedProgramDtos,
|
updatedPrograms,
|
||||||
currentChannel,
|
currentChannel,
|
||||||
ItemUpdateType.MetadataImport,
|
ItemUpdateType.MetadataImport,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
|
await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
currentChannel.IsMovie = isMovie;
|
currentChannel.IsMovie = isMovie;
|
||||||
currentChannel.IsNews = isNews;
|
currentChannel.IsNews = isNews;
|
||||||
@@ -326,7 +325,6 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
var programIds = programs.Select(p => p.Id).ToList();
|
|
||||||
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
|
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,35 +500,27 @@ public class GuideManager : IGuideManager
|
|||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seriesId = info.SeriesId;
|
var channelId = channel.Id;
|
||||||
|
if (!item.ParentId.Equals(channelId))
|
||||||
if (!item.ParentId.Equals(channel.Id))
|
|
||||||
{
|
{
|
||||||
|
item.ParentId = channel.Id;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.ParentId = channel.Id;
|
|
||||||
|
|
||||||
item.Audio = info.Audio;
|
item.Audio = info.Audio;
|
||||||
item.ChannelId = channel.Id;
|
item.ChannelId = channelId;
|
||||||
item.CommunityRating ??= info.CommunityRating;
|
item.CommunityRating = info.CommunityRating;
|
||||||
if ((item.CommunityRating ?? 0).Equals(0))
|
|
||||||
{
|
|
||||||
item.CommunityRating = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.EpisodeTitle = info.EpisodeTitle;
|
item.EpisodeTitle = info.EpisodeTitle;
|
||||||
item.ExternalId = info.Id;
|
item.ExternalId = info.Id;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
|
var seriesId = info.SeriesId;
|
||||||
|
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
item.ExternalSeriesId = seriesId;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.ExternalSeriesId = seriesId;
|
|
||||||
|
|
||||||
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
|
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
|
||||||
|
|
||||||
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
|
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
|
||||||
{
|
{
|
||||||
item.SeriesName = info.Name;
|
item.SeriesName = info.Name;
|
||||||
@@ -578,7 +568,6 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.Tags = tags.ToArray();
|
item.Tags = tags.ToArray();
|
||||||
|
|
||||||
item.Genres = info.Genres.ToArray();
|
item.Genres = info.Genres.ToArray();
|
||||||
|
|
||||||
if (info.IsHD ?? false)
|
if (info.IsHD ?? false)
|
||||||
@@ -589,41 +578,35 @@ public class GuideManager : IGuideManager
|
|||||||
|
|
||||||
item.IsMovie = info.IsMovie;
|
item.IsMovie = info.IsMovie;
|
||||||
item.IsRepeat = info.IsRepeat;
|
item.IsRepeat = info.IsRepeat;
|
||||||
|
|
||||||
if (item.IsSeries != isSeries)
|
if (item.IsSeries != isSeries)
|
||||||
{
|
{
|
||||||
|
item.IsSeries = isSeries;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.IsSeries = isSeries;
|
|
||||||
|
|
||||||
item.Name = info.Name;
|
item.Name = info.Name;
|
||||||
item.OfficialRating ??= info.OfficialRating;
|
item.OfficialRating = info.OfficialRating;
|
||||||
item.Overview ??= info.Overview;
|
item.Overview = info.Overview;
|
||||||
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
|
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
|
||||||
item.ProviderIds = info.ProviderIds;
|
|
||||||
|
|
||||||
foreach (var providerId in info.SeriesProviderIds)
|
foreach (var providerId in info.SeriesProviderIds)
|
||||||
{
|
{
|
||||||
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
|
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.ProviderIds = info.ProviderIds;
|
||||||
if (item.StartDate != info.StartDate)
|
if (item.StartDate != info.StartDate)
|
||||||
{
|
{
|
||||||
|
item.StartDate = info.StartDate;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.StartDate = info.StartDate;
|
|
||||||
|
|
||||||
if (item.EndDate != info.EndDate)
|
if (item.EndDate != info.EndDate)
|
||||||
{
|
{
|
||||||
|
item.EndDate = info.EndDate;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.EndDate = info.EndDate;
|
|
||||||
|
|
||||||
item.ProductionYear = info.ProductionYear;
|
item.ProductionYear = info.ProductionYear;
|
||||||
|
|
||||||
if (!isSeries || info.IsRepeat)
|
if (!isSeries || info.IsRepeat)
|
||||||
{
|
{
|
||||||
item.PremiereDate = info.OriginalAirDate;
|
item.PremiereDate = info.OriginalAirDate;
|
||||||
@@ -632,37 +615,35 @@ public class GuideManager : IGuideManager
|
|||||||
item.IndexNumber = info.EpisodeNumber;
|
item.IndexNumber = info.EpisodeNumber;
|
||||||
item.ParentIndexNumber = info.SeasonNumber;
|
item.ParentIndexNumber = info.SeasonNumber;
|
||||||
|
|
||||||
forceUpdate = forceUpdate || UpdateImages(item, info);
|
forceUpdate |= UpdateImages(item, info);
|
||||||
|
|
||||||
if (isNew)
|
if (isNew)
|
||||||
{
|
{
|
||||||
item.OnMetadataChanged();
|
item.OnMetadataChanged();
|
||||||
|
|
||||||
return (item, isNew, false);
|
return (item, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var isUpdated = false;
|
var isUpdated = forceUpdate;
|
||||||
if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
var etag = info.Etag;
|
||||||
|
if (string.IsNullOrWhiteSpace(etag))
|
||||||
{
|
{
|
||||||
isUpdated = true;
|
isUpdated = true;
|
||||||
}
|
}
|
||||||
else
|
else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var etag = info.Etag;
|
item.SetProviderId(EtagKey, etag);
|
||||||
|
isUpdated = true;
|
||||||
if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
item.SetProviderId(EtagKey, etag);
|
|
||||||
isUpdated = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUpdated)
|
if (isUpdated)
|
||||||
{
|
{
|
||||||
item.OnMetadataChanged();
|
item.OnMetadataChanged();
|
||||||
|
|
||||||
|
return (item, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (item, isNew, isUpdated);
|
return (item, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool UpdateImages(BaseItem item, ProgramInfo info)
|
private static bool UpdateImages(BaseItem item, ProgramInfo info)
|
||||||
@@ -679,7 +660,9 @@ public class GuideManager : IGuideManager
|
|||||||
updated |= UpdateImage(ImageType.Logo, item, info);
|
updated |= UpdateImage(ImageType.Logo, item, info);
|
||||||
|
|
||||||
// Backdrop
|
// Backdrop
|
||||||
return updated || UpdateImage(ImageType.Backdrop, item, info);
|
updated |= UpdateImage(ImageType.Backdrop, item, info);
|
||||||
|
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
|
private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
|
||||||
@@ -689,7 +672,7 @@ public class GuideManager : IGuideManager
|
|||||||
var newImagePath = imageType switch
|
var newImagePath = imageType switch
|
||||||
{
|
{
|
||||||
ImageType.Primary => info.ImagePath,
|
ImageType.Primary => info.ImagePath,
|
||||||
_ => string.Empty
|
_ => null
|
||||||
};
|
};
|
||||||
var newImageUrl = imageType switch
|
var newImageUrl = imageType switch
|
||||||
{
|
{
|
||||||
@@ -697,12 +680,12 @@ public class GuideManager : IGuideManager
|
|||||||
ImageType.Logo => info.LogoImageUrl,
|
ImageType.Logo => info.LogoImageUrl,
|
||||||
ImageType.Primary => info.ImageUrl,
|
ImageType.Primary => info.ImageUrl,
|
||||||
ImageType.Thumb => info.ThumbImageUrl,
|
ImageType.Thumb => info.ThumbImageUrl,
|
||||||
_ => string.Empty
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
|
var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||||
|| newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
|
|| (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
if (!differentImage)
|
if (sameImage)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -757,6 +740,7 @@ public class GuideManager : IGuideManager
|
|||||||
var imageInfo = program.ImageInfos[i];
|
var imageInfo = program.ImageInfos[i];
|
||||||
if (!imageInfo.IsLocalFile)
|
if (!imageInfo.IsLocalFile)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
|
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
|
||||||
|
|||||||
@@ -689,10 +689,10 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
|
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
|
||||||
// If left blank, all remote addresses will be allowed.
|
// If left blank, all remote addresses will be allowed.
|
||||||
if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
|
if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP))
|
||||||
{
|
{
|
||||||
// remoteAddressFilter is a whitelist or blacklist.
|
// remoteAddressFilter is a whitelist or blacklist.
|
||||||
var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
|
var matches = _remoteAddressFilter.Count(remoteNetwork => SubnetContainsAddress(remoteNetwork, remoteIP));
|
||||||
if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|
if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|
||||||
|| (config.IsRemoteIPFilterBlacklist && matches == 0))
|
|| (config.IsRemoteIPFilterBlacklist && matches == 0))
|
||||||
{
|
{
|
||||||
@@ -816,7 +816,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
|
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
|
bool isExternal = !IsInLocalNetwork(source);
|
||||||
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
|
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
|
||||||
|
|
||||||
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
|
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
|
||||||
@@ -863,7 +863,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// (For systems with multiple internal network cards, and multiple subnets)
|
// (For systems with multiple internal network cards, and multiple subnets)
|
||||||
foreach (var intf in availableInterfaces)
|
foreach (var intf in availableInterfaces)
|
||||||
{
|
{
|
||||||
if (intf.Subnet.Contains(source))
|
if (SubnetContainsAddress(intf.Subnet, source))
|
||||||
{
|
{
|
||||||
result = NetworkUtils.FormatIPString(intf.Address);
|
result = NetworkUtils.FormatIPString(intf.Address);
|
||||||
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
|
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
|
||||||
@@ -891,21 +891,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
|
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
|
||||||
{
|
{
|
||||||
return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
|
return IsInLocalNetwork(subnet.Prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
|
return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
|
||||||
{
|
&& addresses.Any(IsInLocalNetwork);
|
||||||
foreach (var ept in addresses)
|
|
||||||
{
|
|
||||||
if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -919,6 +909,19 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
|
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool SubnetContainsAddress(IPNetwork network, IPAddress address)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(address);
|
||||||
|
ArgumentNullException.ThrowIfNull(network);
|
||||||
|
|
||||||
|
if (address.IsIPv4MappedToIPv6)
|
||||||
|
{
|
||||||
|
address = address.MapToIPv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
return network.Contains(address);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsInLocalNetwork(IPAddress address)
|
public bool IsInLocalNetwork(IPAddress address)
|
||||||
{
|
{
|
||||||
@@ -940,6 +943,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
return CheckIfLanAndNotExcluded(address);
|
return CheckIfLanAndNotExcluded(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the address is in the LAN and not excluded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address.</param>
|
||||||
|
/// <returns>Boolean indicates whether the address is in LAN.</returns>
|
||||||
private bool CheckIfLanAndNotExcluded(IPAddress address)
|
private bool CheckIfLanAndNotExcluded(IPAddress address)
|
||||||
{
|
{
|
||||||
foreach (var lanSubnet in _lanSubnets)
|
foreach (var lanSubnet in _lanSubnets)
|
||||||
@@ -979,7 +987,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
// Only use matching internal subnets
|
// Only use matching internal subnets
|
||||||
// Prefer more specific (bigger subnet prefix) overrides
|
// Prefer more specific (bigger subnet prefix) overrides
|
||||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
|
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && SubnetContainsAddress(x.Data.Subnet, source))
|
||||||
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -987,7 +995,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
// Only use matching external subnets
|
// Only use matching external subnets
|
||||||
// Prefer more specific (bigger subnet prefix) overrides
|
// Prefer more specific (bigger subnet prefix) overrides
|
||||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
|
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && SubnetContainsAddress(x.Data.Subnet, source))
|
||||||
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -995,7 +1003,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
foreach (var data in validPublishedServerUrls)
|
foreach (var data in validPublishedServerUrls)
|
||||||
{
|
{
|
||||||
// Get interface matching override subnet
|
// Get interface matching override subnet
|
||||||
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
|
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => SubnetContainsAddress(data.Data.Subnet, x.Address));
|
||||||
|
|
||||||
if (intf?.Address is not null
|
if (intf?.Address is not null
|
||||||
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|
||||||
@@ -1058,6 +1066,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
if (isInExternalSubnet)
|
if (isInExternalSubnet)
|
||||||
{
|
{
|
||||||
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
|
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
|
||||||
|
.Where(x => !IsLinkLocalAddress(x.Address))
|
||||||
.OrderBy(x => x.Index)
|
.OrderBy(x => x.Index)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (externalInterfaces.Count > 0)
|
if (externalInterfaces.Count > 0)
|
||||||
@@ -1065,7 +1074,8 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// Check to see if any of the external bind interfaces are in the same subnet as the source.
|
// Check to see if any of the external bind interfaces are in the same subnet as the source.
|
||||||
// If none exists, this will select the first external interface if there is one.
|
// If none exists, this will select the first external interface if there is one.
|
||||||
bindAddress = externalInterfaces
|
bindAddress = externalInterfaces
|
||||||
.OrderBy(x => x.Subnet.Contains(source))
|
.OrderByDescending(x => SubnetContainsAddress(x.Subnet, source))
|
||||||
|
.ThenByDescending(x => x.Subnet.PrefixLength)
|
||||||
.ThenBy(x => x.Index)
|
.ThenBy(x => x.Index)
|
||||||
.Select(x => x.Address)
|
.Select(x => x.Address)
|
||||||
.First();
|
.First();
|
||||||
@@ -1082,7 +1092,8 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
|
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
|
||||||
// If none exists, this will select the first internal interface if there is one.
|
// If none exists, this will select the first internal interface if there is one.
|
||||||
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
|
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
|
||||||
.OrderBy(x => x.Subnet.Contains(source))
|
.OrderByDescending(x => SubnetContainsAddress(x.Subnet, source))
|
||||||
|
.ThenByDescending(x => x.Subnet.PrefixLength)
|
||||||
.ThenBy(x => x.Index)
|
.ThenBy(x => x.Index)
|
||||||
.Select(x => x.Address)
|
.Select(x => x.Address)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -1125,7 +1136,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// (For systems with multiple network cards and/or multiple subnets)
|
// (For systems with multiple network cards and/or multiple subnets)
|
||||||
foreach (var intf in extResult)
|
foreach (var intf in extResult)
|
||||||
{
|
{
|
||||||
if (intf.Subnet.Contains(source))
|
if (SubnetContainsAddress(intf.Subnet, source))
|
||||||
{
|
{
|
||||||
result = NetworkUtils.FormatIPString(intf.Address);
|
result = NetworkUtils.FormatIPString(intf.Address);
|
||||||
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
|
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
|
||||||
|
|||||||
@@ -141,8 +141,10 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
{ "ProductionYear", 1, 2 },
|
{ "ProductionYear", 1, 2 },
|
||||||
{ "CommunityRating", 1.0f, 2.0f },
|
{ "CommunityRating", 1.0f, 2.0f },
|
||||||
{ "CriticRating", 1.0f, 2.0f },
|
{ "CriticRating", 1.0f, 2.0f },
|
||||||
{ "EndDate", DateTime.UnixEpoch, DateTime.Now },
|
{ "EndDate", DateTime.UnixEpoch, DateTime.UtcNow },
|
||||||
{ "PremiereDate", DateTime.UnixEpoch, DateTime.Now },
|
{ "PremiereDate", DateTime.UnixEpoch, DateTime.UtcNow },
|
||||||
|
{ "PremiereDate", new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow },
|
||||||
|
{ "PremiereDate", new DateTime(2025, 2, 21, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow },
|
||||||
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
|
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +153,15 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
public void MergeBaseItemData_SimpleField_ReplacesAppropriately(string propName, object oldValue, object newValue)
|
public void MergeBaseItemData_SimpleField_ReplacesAppropriately(string propName, object oldValue, object newValue)
|
||||||
{
|
{
|
||||||
// Use type Movie to allow testing of Video3DFormat
|
// Use type Movie to allow testing of Video3DFormat
|
||||||
Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
|
if (propName.Equals("PremiereDate", StringComparison.Ordinal) && oldValue is DateTime oldDateTime)
|
||||||
|
{
|
||||||
|
bool expectReplaced = oldDateTime.Month == 1 && oldDateTime.Day == 1;
|
||||||
|
Assert.Equal(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _), expectReplaced);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
|
||||||
|
}
|
||||||
|
|
||||||
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, true, out _));
|
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, true, out _));
|
||||||
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, null, newValue, null, false, out _));
|
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, null, newValue, null, false, out _));
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Localization;
|
using Emby.Server.Implementations.Localization;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@@ -116,6 +115,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
|||||||
[InlineData("TV-MA", "US", 17)]
|
[InlineData("TV-MA", "US", 17)]
|
||||||
[InlineData("XXX", "asdf", 1000)]
|
[InlineData("XXX", "asdf", 1000)]
|
||||||
[InlineData("Germany: FSK-18", "DE", 18)]
|
[InlineData("Germany: FSK-18", "DE", 18)]
|
||||||
|
[InlineData("Rated : R", "US", 17)]
|
||||||
|
[InlineData("Rated: R", "US", 17)]
|
||||||
|
[InlineData("Rated R", "US", 17)]
|
||||||
|
[InlineData(" PG-13 ", "US", 13)]
|
||||||
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
|
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
|
||||||
{
|
{
|
||||||
var localizationManager = Setup(new ServerConfiguration()
|
var localizationManager = Setup(new ServerConfiguration()
|
||||||
|
|||||||
Reference in New Issue
Block a user