mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
Compare commits
22 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 |
@@ -80,7 +80,7 @@
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||
<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="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<VersionPrefix>10.10.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -15,7 +15,7 @@ TV-PG-DS,10
|
||||
TV-PG-DV,10
|
||||
TV-PG-LS,10
|
||||
TV-PG-LV,10
|
||||
TV-PG-SV,1
|
||||
TV-PG-SV,10
|
||||
TV-PG-DLS,10
|
||||
TV-PG-DLV,10
|
||||
TV-PG-DSV,10
|
||||
|
||||
|
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session
|
||||
private readonly SessionInfo _session;
|
||||
|
||||
private readonly List<IWebSocketConnection> _sockets;
|
||||
private readonly ReaderWriterLockSlim _socketsLock;
|
||||
private bool _disposed = false;
|
||||
|
||||
public WebSocketController(
|
||||
@@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session
|
||||
_logger = logger;
|
||||
_session = session;
|
||||
_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 />
|
||||
public bool SupportsMediaControl => HasOpenSockets;
|
||||
@@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <inheritdoc />
|
||||
public bool IsSessionActive => HasOpenSockets;
|
||||
|
||||
private IEnumerable<IWebSocketConnection> GetActiveSockets()
|
||||
=> _sockets.Where(i => i.State == WebSocketState.Open);
|
||||
|
||||
public void AddWebSocket(IWebSocketConnection connection)
|
||||
{
|
||||
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
|
||||
_sockets.Add(connection);
|
||||
|
||||
connection.Closed += OnConnectionClosed;
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterWriteLock();
|
||||
_sockets.Add(connection);
|
||||
connection.Closed += OnConnectionClosed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||
_sockets.Remove(connection);
|
||||
connection.Closed -= OnConnectionClosed;
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterWriteLock();
|
||||
_sockets.Remove(connection);
|
||||
connection.Closed -= OnConnectionClosed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session
|
||||
T data,
|
||||
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)
|
||||
{
|
||||
@@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var socket in _sockets)
|
||||
try
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
socket.Dispose();
|
||||
_socketsLock.EnterWriteLock();
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
socket.Dispose();
|
||||
}
|
||||
|
||||
_sockets.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_socketsLock.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
@@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var socket in _sockets)
|
||||
try
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
await socket.DisposeAsync().ConfigureAwait(false);
|
||||
_socketsLock.EnterWriteLock();
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
await socket.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_sockets.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_socketsLock.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
||||
/// <param name="message">The message.</param>
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
||||
/// <param name="message">The message.</param>
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<VersionPrefix>10.10.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<VersionPrefix>10.10.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Channels
|
||||
[JsonIgnore]
|
||||
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);
|
||||
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)
|
||||
|
||||
@@ -1299,7 +1299,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetParents().Any(i => !i.IsVisible(user)))
|
||||
if (GetParents().Any(i => !i.IsVisible(user, true)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1521,13 +1521,14 @@ namespace MediaBrowser.Controller.Entities
|
||||
/// Determines if a given user has access to this item.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="ArgumentNullException">If user is null.</exception>
|
||||
public bool IsParentalAllowed(User user)
|
||||
public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
if (!IsVisibleViaTags(user))
|
||||
if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1599,7 +1600,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
private bool IsVisibleViaTags(User user)
|
||||
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
|
||||
{
|
||||
var allTags = GetInheritedTags();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
@@ -1654,13 +1655,14 @@ namespace MediaBrowser.Controller.Entities
|
||||
/// Default is just parental allowed. Can be overridden for more functionality.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <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);
|
||||
|
||||
return IsParentalAllowed(user);
|
||||
return IsParentalAllowed(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
public virtual bool IsVisibleStandalone(User user)
|
||||
|
||||
@@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities
|
||||
return GetLibraryOptions(Path);
|
||||
}
|
||||
|
||||
public override bool IsVisible(User user)
|
||||
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||
{
|
||||
if (GetLibraryOptions().Enabled)
|
||||
{
|
||||
return base.IsVisible(user);
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
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)
|
||||
{
|
||||
@@ -239,7 +239,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
}
|
||||
|
||||
return base.IsVisible(user);
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -144,14 +144,14 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
return GetItemLookupInfo<BoxSetInfo>();
|
||||
}
|
||||
|
||||
public override bool IsVisible(User user)
|
||||
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||
{
|
||||
if (IsLegacyBoxSet)
|
||||
{
|
||||
return base.IsVisible(user);
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
if (base.IsVisible(user))
|
||||
if (base.IsVisible(user, skipAllowedTagsCheck))
|
||||
{
|
||||
if (LinkedChildren.Length == 0)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<VersionPrefix>10.10.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -227,11 +227,11 @@ namespace MediaBrowser.Controller.Playlists
|
||||
return [item];
|
||||
}
|
||||
|
||||
public override bool IsVisible(User user)
|
||||
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||
{
|
||||
if (!IsSharedItem)
|
||||
{
|
||||
return base.IsVisible(user);
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
if (OpenAccess)
|
||||
|
||||
@@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
|
||||
_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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<VersionPrefix>10.10.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -551,10 +552,16 @@ namespace MediaBrowser.Providers.Manager
|
||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).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(
|
||||
item,
|
||||
stream,
|
||||
response.Content.Headers.ContentType?.MediaType,
|
||||
mimetype,
|
||||
type,
|
||||
null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -677,10 +684,16 @@ namespace MediaBrowser.Providers.Manager
|
||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).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(
|
||||
item,
|
||||
stream,
|
||||
response.Content.Headers.ContentType?.MediaType,
|
||||
mimetype,
|
||||
imageType,
|
||||
null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -204,20 +204,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
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...
|
||||
if (contentType.Equals(MediaTypeNames.Text.Html, 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)))
|
||||
// some providers don't correctly report media type, extract from url if no extension found
|
||||
if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Strip query parameters from url to get actual path.
|
||||
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
|
||||
@@ -225,7 +215,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
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);
|
||||
|
||||
@@ -172,9 +172,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
||||
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
||||
track.Year ??= mediaInfo.ProductionYear;
|
||||
track.TrackNumber ??= mediaInfo.IndexNumber;
|
||||
track.DiscNumber ??= mediaInfo.ParentIndexNumber;
|
||||
track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
|
||||
track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
|
||||
track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
|
||||
|
||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.10.5")]
|
||||
[assembly: AssemblyFileVersion("10.10.5")]
|
||||
[assembly: AssemblyVersion("10.10.6")]
|
||||
[assembly: AssemblyFileVersion("10.10.6")]
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
|
||||
if (semaphoreCount < 1)
|
||||
{
|
||||
semaphoreCount = 2 * Environment.ProcessorCount;
|
||||
semaphoreCount = Environment.ProcessorCount;
|
||||
}
|
||||
|
||||
_parallelEncodingLimit = new(semaphoreCount);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<VersionPrefix>10.10.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities.Libraries;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.LiveTv.Configuration;
|
||||
@@ -210,7 +209,7 @@ public class GuideManager : IGuideManager
|
||||
progress.Report(15);
|
||||
|
||||
numComplete = 0;
|
||||
var programs = new List<LiveTvProgram>();
|
||||
var programIds = new List<Guid>();
|
||||
var channels = new List<Guid>();
|
||||
|
||||
var guideDays = GetGuideDays();
|
||||
@@ -243,8 +242,8 @@ public class GuideManager : IGuideManager
|
||||
DtoOptions = new DtoOptions(true)
|
||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||
|
||||
var newPrograms = new List<Guid>();
|
||||
var updatedPrograms = new List<Guid>();
|
||||
var newPrograms = new List<LiveTvProgram>();
|
||||
var updatedPrograms = new List<LiveTvProgram>();
|
||||
|
||||
foreach (var program in channelPrograms)
|
||||
{
|
||||
@@ -252,14 +251,14 @@ public class GuideManager : IGuideManager
|
||||
var id = programItem.Id;
|
||||
if (isNew)
|
||||
{
|
||||
newPrograms.Add(id);
|
||||
newPrograms.Add(programItem);
|
||||
}
|
||||
else if (isUpdated)
|
||||
{
|
||||
updatedPrograms.Add(id);
|
||||
updatedPrograms.Add(programItem);
|
||||
}
|
||||
|
||||
programs.Add(programItem);
|
||||
programIds.Add(programItem.Id);
|
||||
|
||||
isMovie |= program.IsMovie;
|
||||
isSeries |= program.IsSeries;
|
||||
@@ -276,21 +275,21 @@ public class GuideManager : IGuideManager
|
||||
|
||||
if (newPrograms.Count > 0)
|
||||
{
|
||||
var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
|
||||
_libraryManager.CreateItems(newProgramDtos, null, cancellationToken);
|
||||
_libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken);
|
||||
|
||||
await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (updatedPrograms.Count > 0)
|
||||
{
|
||||
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
|
||||
await _libraryManager.UpdateItemsAsync(
|
||||
updatedProgramDtos,
|
||||
updatedPrograms,
|
||||
currentChannel,
|
||||
ItemUpdateType.MetadataImport,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
|
||||
await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
currentChannel.IsMovie = isMovie;
|
||||
currentChannel.IsNews = isNews;
|
||||
@@ -326,7 +325,6 @@ public class GuideManager : IGuideManager
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
var programIds = programs.Select(p => p.Id).ToList();
|
||||
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
|
||||
}
|
||||
|
||||
@@ -502,35 +500,27 @@ public class GuideManager : IGuideManager
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
var seriesId = info.SeriesId;
|
||||
|
||||
if (!item.ParentId.Equals(channel.Id))
|
||||
var channelId = channel.Id;
|
||||
if (!item.ParentId.Equals(channelId))
|
||||
{
|
||||
item.ParentId = channel.Id;
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.ParentId = channel.Id;
|
||||
|
||||
item.Audio = info.Audio;
|
||||
item.ChannelId = channel.Id;
|
||||
item.CommunityRating ??= info.CommunityRating;
|
||||
if ((item.CommunityRating ?? 0).Equals(0))
|
||||
{
|
||||
item.CommunityRating = null;
|
||||
}
|
||||
|
||||
item.ChannelId = channelId;
|
||||
item.CommunityRating = info.CommunityRating;
|
||||
item.EpisodeTitle = info.EpisodeTitle;
|
||||
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;
|
||||
}
|
||||
|
||||
item.ExternalSeriesId = seriesId;
|
||||
|
||||
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
|
||||
|
||||
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
|
||||
{
|
||||
item.SeriesName = info.Name;
|
||||
@@ -578,7 +568,6 @@ public class GuideManager : IGuideManager
|
||||
}
|
||||
|
||||
item.Tags = tags.ToArray();
|
||||
|
||||
item.Genres = info.Genres.ToArray();
|
||||
|
||||
if (info.IsHD ?? false)
|
||||
@@ -589,41 +578,35 @@ public class GuideManager : IGuideManager
|
||||
|
||||
item.IsMovie = info.IsMovie;
|
||||
item.IsRepeat = info.IsRepeat;
|
||||
|
||||
if (item.IsSeries != isSeries)
|
||||
{
|
||||
item.IsSeries = isSeries;
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.IsSeries = isSeries;
|
||||
|
||||
item.Name = info.Name;
|
||||
item.OfficialRating ??= info.OfficialRating;
|
||||
item.Overview ??= info.Overview;
|
||||
item.OfficialRating = info.OfficialRating;
|
||||
item.Overview = info.Overview;
|
||||
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
|
||||
item.ProviderIds = info.ProviderIds;
|
||||
|
||||
foreach (var providerId in info.SeriesProviderIds)
|
||||
{
|
||||
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
|
||||
}
|
||||
|
||||
item.ProviderIds = info.ProviderIds;
|
||||
if (item.StartDate != info.StartDate)
|
||||
{
|
||||
item.StartDate = info.StartDate;
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.StartDate = info.StartDate;
|
||||
|
||||
if (item.EndDate != info.EndDate)
|
||||
{
|
||||
item.EndDate = info.EndDate;
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.EndDate = info.EndDate;
|
||||
|
||||
item.ProductionYear = info.ProductionYear;
|
||||
|
||||
if (!isSeries || info.IsRepeat)
|
||||
{
|
||||
item.PremiereDate = info.OriginalAirDate;
|
||||
@@ -632,37 +615,35 @@ public class GuideManager : IGuideManager
|
||||
item.IndexNumber = info.EpisodeNumber;
|
||||
item.ParentIndexNumber = info.SeasonNumber;
|
||||
|
||||
forceUpdate = forceUpdate || UpdateImages(item, info);
|
||||
forceUpdate |= UpdateImages(item, info);
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
item.OnMetadataChanged();
|
||||
|
||||
return (item, isNew, false);
|
||||
return (item, true, false);
|
||||
}
|
||||
|
||||
var isUpdated = false;
|
||||
if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
||||
var isUpdated = forceUpdate;
|
||||
var etag = info.Etag;
|
||||
if (string.IsNullOrWhiteSpace(etag))
|
||||
{
|
||||
isUpdated = true;
|
||||
}
|
||||
else
|
||||
else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var etag = info.Etag;
|
||||
|
||||
if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.SetProviderId(EtagKey, etag);
|
||||
isUpdated = true;
|
||||
}
|
||||
item.SetProviderId(EtagKey, etag);
|
||||
isUpdated = true;
|
||||
}
|
||||
|
||||
if (isUpdated)
|
||||
{
|
||||
item.OnMetadataChanged();
|
||||
|
||||
return (item, false, true);
|
||||
}
|
||||
|
||||
return (item, isNew, isUpdated);
|
||||
return (item, false, false);
|
||||
}
|
||||
|
||||
private static bool UpdateImages(BaseItem item, ProgramInfo info)
|
||||
@@ -679,7 +660,9 @@ public class GuideManager : IGuideManager
|
||||
updated |= UpdateImage(ImageType.Logo, item, info);
|
||||
|
||||
// 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)
|
||||
@@ -689,7 +672,7 @@ public class GuideManager : IGuideManager
|
||||
var newImagePath = imageType switch
|
||||
{
|
||||
ImageType.Primary => info.ImagePath,
|
||||
_ => string.Empty
|
||||
_ => null
|
||||
};
|
||||
var newImageUrl = imageType switch
|
||||
{
|
||||
@@ -697,12 +680,12 @@ public class GuideManager : IGuideManager
|
||||
ImageType.Logo => info.LogoImageUrl,
|
||||
ImageType.Primary => info.ImageUrl,
|
||||
ImageType.Thumb => info.ThumbImageUrl,
|
||||
_ => string.Empty
|
||||
_ => null
|
||||
};
|
||||
|
||||
var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
|
||||
|| newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
|
||||
if (!differentImage)
|
||||
var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
|| (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
if (sameImage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -757,6 +740,7 @@ public class GuideManager : IGuideManager
|
||||
var imageInfo = program.ImageInfos[i];
|
||||
if (!imageInfo.IsLocalFile)
|
||||
{
|
||||
_logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
|
||||
try
|
||||
{
|
||||
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.
|
||||
// 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.
|
||||
var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
|
||||
var matches = _remoteAddressFilter.Count(remoteNetwork => SubnetContainsAddress(remoteNetwork, remoteIP));
|
||||
if ((!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.");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
foreach (var intf in availableInterfaces)
|
||||
{
|
||||
if (intf.Subnet.Contains(source))
|
||||
if (SubnetContainsAddress(intf.Subnet, source))
|
||||
{
|
||||
result = NetworkUtils.FormatIPString(intf.Address);
|
||||
_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))
|
||||
{
|
||||
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))
|
||||
{
|
||||
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;
|
||||
return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
|
||||
&& addresses.Any(IsInLocalNetwork);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -919,6 +909,19 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
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/>
|
||||
public bool IsInLocalNetwork(IPAddress address)
|
||||
{
|
||||
@@ -940,6 +943,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
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)
|
||||
{
|
||||
foreach (var lanSubnet in _lanSubnets)
|
||||
@@ -979,7 +987,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
{
|
||||
// Only use matching internal subnets
|
||||
// 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)
|
||||
.ToList();
|
||||
}
|
||||
@@ -987,7 +995,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
{
|
||||
// Only use matching external subnets
|
||||
// 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)
|
||||
.ToList();
|
||||
}
|
||||
@@ -995,7 +1003,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
foreach (var data in validPublishedServerUrls)
|
||||
{
|
||||
// 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
|
||||
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|
||||
@@ -1058,6 +1066,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
if (isInExternalSubnet)
|
||||
{
|
||||
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
|
||||
.Where(x => !IsLinkLocalAddress(x.Address))
|
||||
.OrderBy(x => x.Index)
|
||||
.ToList();
|
||||
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.
|
||||
// If none exists, this will select the first external interface if there is one.
|
||||
bindAddress = externalInterfaces
|
||||
.OrderBy(x => x.Subnet.Contains(source))
|
||||
.OrderByDescending(x => SubnetContainsAddress(x.Subnet, source))
|
||||
.ThenByDescending(x => x.Subnet.PrefixLength)
|
||||
.ThenBy(x => x.Index)
|
||||
.Select(x => x.Address)
|
||||
.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.
|
||||
// If none exists, this will select the first internal interface if there is one.
|
||||
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)
|
||||
.Select(x => x.Address)
|
||||
.FirstOrDefault();
|
||||
@@ -1125,7 +1136,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
// (For systems with multiple network cards and/or multiple subnets)
|
||||
foreach (var intf in extResult)
|
||||
{
|
||||
if (intf.Subnet.Contains(source))
|
||||
if (SubnetContainsAddress(intf.Subnet, source))
|
||||
{
|
||||
result = NetworkUtils.FormatIPString(intf.Address);
|
||||
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
|
||||
|
||||
Reference in New Issue
Block a user