Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5b771861f | |||
| 3f539472f3 | |||
| 7f43521b64 | |||
| 99006c370f | |||
| e3f9f0a7f3 | |||
| d1fbdcee34 | |||
| 21e398ba0c | |||
| 8544e7fc72 | |||
| 117d2082aa | |||
| 03082e90f9 | |||
| 88026518b1 | |||
| 5f1fb26382 | |||
| 070d04c1b2 | |||
| 8aa4e2e320 | |||
| 49bb5a6442 | |||
| 9e869b4541 | |||
| 710e877762 | |||
| f536e08e14 | |||
| 4eecfee29f | |||
| 731874429c | |||
| e6c6441abf | |||
| 4d89a095ed | |||
| 1136a36eed | |||
| e8514de33b | |||
| 722cdcce5e | |||
| bfe0fdbcdc | |||
| 0b2a59e963 | |||
| 6329de4fc3 | |||
| 644df3585b | |||
| 3766a88bea | |||
| f333ef74b3 | |||
| 0394965753 | |||
| 53a45c6033 | |||
| adfe52f55a | |||
| c693da94ce | |||
| 1a7c2299c6 | |||
| 9c7d735a96 | |||
| 5df03b9010 | |||
| ef13a18450 |
@@ -9,8 +9,8 @@
|
|||||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
|
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||||
<PackageVersion Include="BlurHashSharp" Version="1.3.3" />
|
<PackageVersion Include="BlurHashSharp" Version="1.3.4" />
|
||||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||||
@@ -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.16.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.4</VersionPrefix>
|
<VersionPrefix>10.10.6</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>
|
||||||
|
|||||||
@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
// IOException generally means the file is not accessible due to filesystem issues
|
||||||
|
// Catch this exception and mark the file as not exist to ignore it
|
||||||
|
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
|
||||||
|
result.Exists = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +597,9 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
|
// Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
|
||||||
|
// But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
|
||||||
|
// For example, the scanner will remove everything in that path due to unhandled errors.
|
||||||
var directoryInfo = new DirectoryInfo(path);
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
Livre,0
|
Livre,0
|
||||||
L,0
|
L,0
|
||||||
ER,9
|
AL,0
|
||||||
|
ER,10
|
||||||
10,10
|
10,10
|
||||||
|
A10,10
|
||||||
12,12
|
12,12
|
||||||
|
A12,12
|
||||||
14,14
|
14,14
|
||||||
|
A14,14
|
||||||
16,16
|
16,16
|
||||||
|
A16,16
|
||||||
18,18
|
18,18
|
||||||
|
A18,18
|
||||||
|
|||||||
|
@@ -6,8 +6,6 @@ TV-Y7,7
|
|||||||
TV-Y7-FV,7
|
TV-Y7-FV,7
|
||||||
PG,9
|
PG,9
|
||||||
TV-PG,9
|
TV-PG,9
|
||||||
PG-13,13
|
|
||||||
13+,13
|
|
||||||
TV-14,14
|
TV-14,14
|
||||||
14A,14
|
14A,14
|
||||||
16+,16
|
16+,16
|
||||||
|
|||||||
|
@@ -1,7 +1,7 @@
|
|||||||
A,0
|
A,0
|
||||||
A/fig,0
|
A/fig,0
|
||||||
A/i,0
|
A/i,0
|
||||||
A/fig/i,0
|
A/i/fig,0
|
||||||
APTA,0
|
APTA,0
|
||||||
ERI,0
|
ERI,0
|
||||||
TP,0
|
TP,0
|
||||||
|
|||||||
|
@@ -6,10 +6,11 @@ U,0
|
|||||||
6+,6
|
6+,6
|
||||||
7+,7
|
7+,7
|
||||||
PG,8
|
PG,8
|
||||||
9+,9
|
9,9
|
||||||
12,12
|
12,12
|
||||||
12+,12
|
12+,12
|
||||||
12A,12
|
12A,12
|
||||||
|
12PG,12
|
||||||
Teen,13
|
Teen,13
|
||||||
13+,13
|
13+,13
|
||||||
14+,14
|
14+,14
|
||||||
|
|||||||
|
@@ -4,6 +4,7 @@ PG,12
|
|||||||
12A,12
|
12A,12
|
||||||
12PG,12
|
12PG,12
|
||||||
15,15
|
15,15
|
||||||
|
15PG,15
|
||||||
15A,15
|
15A,15
|
||||||
16,16
|
16,16
|
||||||
18,18
|
18,18
|
||||||
|
|||||||
|
@@ -6,4 +6,5 @@ A,0
|
|||||||
12,12
|
12,12
|
||||||
15,15
|
15,15
|
||||||
18,18
|
18,18
|
||||||
|
C,18
|
||||||
Not approved,1001
|
Not approved,1001
|
||||||
|
|||||||
|
@@ -10,6 +10,7 @@ R16,16
|
|||||||
RP16,16
|
RP16,16
|
||||||
GA,18
|
GA,18
|
||||||
R18,18
|
R18,18
|
||||||
|
RP18,18
|
||||||
MA,1000
|
MA,1000
|
||||||
R,1001
|
R,1001
|
||||||
Objectionable,1001
|
Objectionable,1001
|
||||||
|
|||||||
|
@@ -5,23 +5,23 @@ TV-Y,0
|
|||||||
TV-Y7,7
|
TV-Y7,7
|
||||||
TV-Y7-FV,7
|
TV-Y7-FV,7
|
||||||
PG,10
|
PG,10
|
||||||
|
TV-PG,10
|
||||||
|
TV-PG-D,10
|
||||||
|
TV-PG-L,10
|
||||||
|
TV-PG-S,10
|
||||||
|
TV-PG-V,10
|
||||||
|
TV-PG-DL,10
|
||||||
|
TV-PG-DS,10
|
||||||
|
TV-PG-DV,10
|
||||||
|
TV-PG-LS,10
|
||||||
|
TV-PG-LV,10
|
||||||
|
TV-PG-SV,10
|
||||||
|
TV-PG-DLS,10
|
||||||
|
TV-PG-DLV,10
|
||||||
|
TV-PG-DSV,10
|
||||||
|
TV-PG-LSV,10
|
||||||
|
TV-PG-DLSV,10
|
||||||
PG-13,13
|
PG-13,13
|
||||||
TV-PG,13
|
|
||||||
TV-PG-D,13
|
|
||||||
TV-PG-L,13
|
|
||||||
TV-PG-S,13
|
|
||||||
TV-PG-V,13
|
|
||||||
TV-PG-DL,13
|
|
||||||
TV-PG-DS,13
|
|
||||||
TV-PG-DV,13
|
|
||||||
TV-PG-LS,13
|
|
||||||
TV-PG-LV,13
|
|
||||||
TV-PG-SV,13
|
|
||||||
TV-PG-DLS,13
|
|
||||||
TV-PG-DLV,13
|
|
||||||
TV-PG-DSV,13
|
|
||||||
TV-PG-LSV,13
|
|
||||||
TV-PG-DLSV,13
|
|
||||||
TV-14,14
|
TV-14,14
|
||||||
TV-14-D,14
|
TV-14-D,14
|
||||||
TV-14-L,14
|
TV-14-L,14
|
||||||
@@ -48,3 +48,5 @@ TV-MA-LS,17
|
|||||||
TV-MA-LV,17
|
TV-MA-LV,17
|
||||||
TV-MA-SV,17
|
TV-MA-SV,17
|
||||||
TV-MA-LSV,17
|
TV-MA-LSV,17
|
||||||
|
TV-X,18
|
||||||
|
TV-AO,18
|
||||||
|
|||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.4</VersionPrefix>
|
<VersionPrefix>10.10.6</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>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ namespace Jellyfin.Server.Migrations
|
|||||||
typeof(Routines.RemoveDuplicatePlaylistChildren)
|
typeof(Routines.RemoveDuplicatePlaylistChildren)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly Guid _downgradeCheckMigration = Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Run all needed migrations.
|
/// Run all needed migrations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -88,6 +90,12 @@ namespace Jellyfin.Server.Migrations
|
|||||||
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
|
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
|
||||||
: new MigrationOptions();
|
: new MigrationOptions();
|
||||||
|
|
||||||
|
// 10.10 specific EFCore migration check.
|
||||||
|
if (migrationOptions.Applied.Any(f => f.Id.Equals(_downgradeCheckMigration)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("You cannot downgrade your jellyfin install from the library.db migration.");
|
||||||
|
}
|
||||||
|
|
||||||
// We have to deserialize it manually since the configuration manager may overwrite it
|
// We have to deserialize it manually since the configuration manager may overwrite it
|
||||||
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
|
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
|
||||||
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
|
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
|
public Guid Id => Guid.Parse("{D34BFC33-5D2E-4790-8085-069EF6EECB4E}");
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Name => "MigrateRatingLevels";
|
public string Name => "MigrateRatingLevels";
|
||||||
|
|||||||
@@ -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.4</VersionPrefix>
|
<VersionPrefix>10.10.6</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>
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.4</VersionPrefix>
|
<VersionPrefix>10.10.6</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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
public class SubtitleEditParser : ISubtitleParser
|
public class SubtitleEditParser : ISubtitleParser
|
||||||
{
|
{
|
||||||
private readonly ILogger<SubtitleEditParser> _logger;
|
private readonly ILogger<SubtitleEditParser> _logger;
|
||||||
private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
|
private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
|
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
|
||||||
@@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
|
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_subtitleFormats = GetSubtitleFormats()
|
_subtitleFormatTypes = GetSubtitleFormatTypes();
|
||||||
.Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
|
|
||||||
.GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
var subtitle = new Subtitle();
|
var subtitle = new Subtitle();
|
||||||
var lines = stream.ReadAllLines().ToList();
|
var lines = stream.ReadAllLines().ToList();
|
||||||
|
|
||||||
if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
|
if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
|
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var subtitleFormat in subtitleFormats)
|
foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
|
||||||
{
|
{
|
||||||
|
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
||||||
fileExtension,
|
fileExtension,
|
||||||
@@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool SupportsFileExtension(string fileExtension)
|
public bool SupportsFileExtension(string fileExtension)
|
||||||
=> _subtitleFormats.ContainsKey(fileExtension);
|
=> _subtitleFormatTypes.ContainsKey(fileExtension);
|
||||||
|
|
||||||
private List<SubtitleFormat> GetSubtitleFormats()
|
private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
|
||||||
{
|
{
|
||||||
var subtitleFormats = new List<SubtitleFormat>();
|
var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
|
||||||
var assembly = typeof(SubtitleFormat).Assembly;
|
var assembly = typeof(SubtitleFormat).Assembly;
|
||||||
|
|
||||||
foreach (var type in assembly.GetTypes())
|
foreach (var type in assembly.GetTypes())
|
||||||
@@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// It shouldn't be null, but the exception is caught if it is
|
var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
||||||
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
var extension = tempInstance.Extension.TrimStart('.');
|
||||||
subtitleFormats.Add(subtitleFormat);
|
if (!string.IsNullOrEmpty(extension))
|
||||||
|
{
|
||||||
|
// Store only the type, we will instantiate from it later
|
||||||
|
if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
|
||||||
|
{
|
||||||
|
subtitleFormatTypes[extension] = [type];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
subtitleFormatTypesForExtension.Add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return subtitleFormats;
|
return subtitleFormatTypes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1001,6 +1001,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}))
|
}))
|
||||||
.All(satisfied => satisfied);
|
.All(satisfied => satisfied);
|
||||||
|
|
||||||
|
directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit);
|
||||||
|
|
||||||
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
|
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
|
||||||
|
|
||||||
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
||||||
|
|||||||
@@ -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.4</VersionPrefix>
|
<VersionPrefix>10.10.6</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>
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var fileStreamOptions = AsyncFile.WriteOptions;
|
var fileStreamOptions = AsyncFile.WriteOptions;
|
||||||
fileStreamOptions.Mode = FileMode.Create;
|
fileStreamOptions.Mode = FileMode.Create;
|
||||||
|
fileStreamOptions.Options = FileOptions.WriteThrough;
|
||||||
if (source.CanSeek)
|
if (source.CanSeek)
|
||||||
{
|
{
|
||||||
fileStreamOptions.PreallocationSize = source.Length;
|
fileStreamOptions.PreallocationSize = source.Length;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -172,9 +172,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
||||||
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
||||||
track.Year ??= mediaInfo.ProductionYear;
|
track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
|
||||||
track.TrackNumber ??= mediaInfo.IndexNumber;
|
track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
|
||||||
track.DiscNumber ??= mediaInfo.ParentIndexNumber;
|
track.DiscNumber = 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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
|
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
|
||||||
{
|
{
|
||||||
// TODO Handle this better later
|
try
|
||||||
if (!val.Contains('<', StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
movie.CollectionName = val;
|
ParseSetXml(val, movie);
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
try
|
Logger.LogError(ex, "Error parsing set node");
|
||||||
{
|
|
||||||
ParseSetXml(val, movie);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error parsing set node");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +128,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
// Loop through each element
|
// Loop through each element
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||||
{
|
{
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
if (reader.NodeType == XmlNodeType.Text && reader.Depth == 1)
|
||||||
|
{
|
||||||
|
movie.CollectionName = reader.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (reader.NodeType == XmlNodeType.Element)
|
||||||
{
|
{
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(movie.CollectionName))
|
if (!string.IsNullOrEmpty(movie.CollectionName))
|
||||||
{
|
{
|
||||||
writer.WriteElementString("set", movie.CollectionName);
|
writer.WriteStartElement("set");
|
||||||
|
writer.WriteElementString("name", movie.CollectionName);
|
||||||
|
writer.WriteEndElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.10.4")]
|
[assembly: AssemblyVersion("10.10.6")]
|
||||||
[assembly: AssemblyFileVersion("10.10.4")]
|
[assembly: AssemblyFileVersion("10.10.6")]
|
||||||
|
|||||||
@@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
|
||||||
|
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
||||||
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool RequiresSpecialCharacterHack(string path)
|
private bool RequiresSpecialCharacterHack(string path)
|
||||||
|
|||||||
@@ -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.4</VersionPrefix>
|
<VersionPrefix>10.10.6</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,9 +1003,11 @@ 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.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
|
||||||
{
|
{
|
||||||
// If matching interface is found, use override
|
// If matching interface is found, use override
|
||||||
bindPreference = data.OverrideUri;
|
bindPreference = data.OverrideUri;
|
||||||
@@ -1025,6 +1035,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
|
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,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)
|
||||||
@@ -1062,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
|
||||||
.OrderByDescending(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();
|
||||||
@@ -1079,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))
|
||||||
.OrderByDescending(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();
|
||||||
@@ -1122,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);
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
|||||||
await localizationManager.LoadAll();
|
await localizationManager.LoadAll();
|
||||||
var ratings = localizationManager.GetParentalRatings().ToList();
|
var ratings = localizationManager.GetParentalRatings().ToList();
|
||||||
|
|
||||||
Assert.Equal(54, ratings.Count);
|
Assert.Equal(56, ratings.Count);
|
||||||
|
|
||||||
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
|
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
|
||||||
Assert.NotNull(tvma);
|
Assert.NotNull(tvma);
|
||||||
|
|||||||
@@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
|||||||
|
|
||||||
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
|
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parsing_Fields_With_Escaped_Xml_Special_Characters_Success()
|
||||||
|
{
|
||||||
|
var result = new MetadataResult<Video>()
|
||||||
|
{
|
||||||
|
Item = new Movie()
|
||||||
|
};
|
||||||
|
|
||||||
|
_parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
|
||||||
|
var item = (Movie)result.Item;
|
||||||
|
|
||||||
|
Assert.Equal("Lilo & Stitch", item.Name);
|
||||||
|
Assert.Equal("Lilo & Stitch", item.OriginalTitle);
|
||||||
|
Assert.Equal("Lilo & Stitch Collection", item.CollectionName);
|
||||||
|
Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
|
||||||
|
Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
|
<movie>
|
||||||
|
<title>Lilo & Stitch</title>
|
||||||
|
<originaltitle>Lilo & Stitch</originaltitle>
|
||||||
|
<set>Lilo & Stitch Collection</set>
|
||||||
|
<plot>>>As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.<<</plot>
|
||||||
|
</movie>
|
||||||
Reference in New Issue
Block a user