aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs14
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs36
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs28
-rw-r--r--MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs152
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj1
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs18
-rw-r--r--MediaBrowser.Controller/Persistence/IItemCountService.cs4
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs111
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs13
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs12
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs5
12 files changed, 329 insertions, 67 deletions
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index e85f86b72f..eb2a3676ac 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -103,6 +103,7 @@ namespace MediaBrowser.Controller.Entities
|| SubtitleLanguages.Count > 0
|| LinkedChildAncestorIds.Length > 0
|| AncestorIds.Length > 0
+ || DescendantOfId.HasValue
|| IsFavorite.HasValue
|| IsFavoriteOrLiked.HasValue
|| IsLiked.HasValue
@@ -368,6 +369,13 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public Guid[] LinkedChildAncestorIds { get; set; }
+ /// <summary>
+ /// Gets or sets the id of a folder whose descendants the items must be part of.
+ /// Unlike <see cref="AncestorIds"/> this also follows the linked children of BoxSets and
+ /// Playlists, so it reaches the items below a linked folder (a Series' episodes, for example).
+ /// </summary>
+ public Guid? DescendantOfId { get; set; }
+
public Guid[] TopParentIds { get; set; }
public CollectionType?[] PresetViews { get; set; }
@@ -424,12 +432,18 @@ namespace MediaBrowser.Controller.Entities
public string? HasNoSubtitleTrackWithLanguage { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to return only items nothing names any more.
+ /// </summary>
public bool? IsDeadArtist { get; set; }
public bool? IsDeadStudio { get; set; }
public bool? IsDeadGenre { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to return only items nothing names any more.
+ /// </summary>
public bool? IsDeadPerson { get; set; }
/// <summary>
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
index 44b7fadf5e..b2d2273cbe 100644
--- a/MediaBrowser.Controller/IO/FileSystemHelper.cs
+++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs
@@ -166,4 +166,40 @@ public static class FileSystemHelper
return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget);
}
+
+ /// <summary>
+ /// Combines a caller supplied name with a parent directory, making sure the name cannot escape that directory.
+ /// </summary>
+ /// <param name="parentPath">The directory the name has to resolve inside of.</param>
+ /// <param name="name">The name of the child.</param>
+ /// <returns>
+ /// The full path of the child, or <c>null</c> if <paramref name="name"/> is not the name of a direct child
+ /// of <paramref name="parentPath"/>.
+ /// </returns>
+ public static string? GetChildPath(string parentPath, string name)
+ {
+ if (string.IsNullOrWhiteSpace(name) || name.Contains('\0', StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ // Rejects directory separators, and on Windows also volume separators, as those make the name more than a single segment.
+ if (!string.Equals(Path.GetFileName(name), name, StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ var fullPath = Path.GetFullPath(Path.Combine(parentPath, name));
+ var fullParentPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(parentPath));
+
+ // Catches the remaining relative names, "." and "..", which are valid single segments.
+ if (!string.Equals(Path.GetDirectoryName(fullPath), fullParentPath, StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ // Windows strips trailing dots and spaces, so a name like "..." resolves to the parent directory itself
+ // and a name like "Movies." to a different child. Reject anything normalization did not leave intact.
+ return string.Equals(Path.GetFileName(fullPath), name, StringComparison.Ordinal) ? fullPath : null;
+ }
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 2a6ea214b8..9028b0d6b8 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -107,6 +107,13 @@ namespace MediaBrowser.Controller.Library
Person? GetPerson(string name);
/// <summary>
+ /// Gets a Person, creating and persisting it if no item exists for the name yet.
+ /// </summary>
+ /// <param name="name">The name of the person.</param>
+ /// <returns>The person.</returns>
+ Person GetOrCreatePerson(string name);
+
+ /// <summary>
/// Finds the by path.
/// </summary>
/// <param name="path">The path.</param>
@@ -153,15 +160,6 @@ namespace MediaBrowser.Controller.Library
Year GetYear(int value);
/// <summary>
- /// Validate and refresh the People sub-set of the IBN.
- /// The items are stored in the db but not loaded into memory until actually requested by an operation.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken);
-
- /// <summary>
/// Reloads the root media folder.
/// </summary>
/// <param name="progress">The progress.</param>
@@ -709,6 +707,14 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if ignored, <c>false</c> otherwise.</returns>
bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
+ /// <summary>
+ /// Gets the id a <see cref="Person"/> item for the name would have, without looking it up
+ /// or creating it.
+ /// </summary>
+ /// <param name="name">The name of the person.</param>
+ /// <returns>The item id for the name.</returns>
+ Guid GetPersonId(string name);
+
Guid GetStudioId(string name);
Guid GetGenreId(string name);
@@ -758,9 +764,9 @@ namespace MediaBrowser.Controller.Library
/// Returns the count of immediate children (non-recursive) for each parent.
/// </summary>
/// <param name="parentIds">The list of parent folder IDs.</param>
- /// <param name="userId">The user ID for access filtering.</param>
+ /// <param name="user">The user the counts are for, or null to count without a user's preferences.</param>
/// <returns>Dictionary mapping parent ID to child count.</returns>
- Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId);
+ Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user);
/// <summary>
/// Batch-fetches played and total counts for multiple folder items.
diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
index 6da398129a..be75117b6f 100644
--- a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
+++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
@@ -17,7 +17,8 @@ namespace MediaBrowser.Controller.LibraryTaskScheduler;
/// </summary>
public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibraryScheduler, IAsyncDisposable
{
- private const int CleanupGracePeriod = 60;
+ private static readonly TimeSpan _cleanupGracePeriod = TimeSpan.FromSeconds(60);
+
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<LimitedConcurrencyLibraryScheduler> _logger;
private readonly IServerConfigurationManager _serverConfigurationManager;
@@ -31,6 +32,8 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
private readonly Lock _taskLock = new();
private readonly Channel<TaskQueueItem> _tasks = Channel.CreateUnbounded<TaskQueueItem>();
+ private readonly CancellationTokenSource _disposeTokenSource = new();
+ private readonly TimeSpan _gracePeriod;
private volatile int _workCounter;
private Task? _cleanupTask;
@@ -46,10 +49,34 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
IHostApplicationLifetime hostApplicationLifetime,
ILogger<LimitedConcurrencyLibraryScheduler> logger,
IServerConfigurationManager serverConfigurationManager)
+ : this(hostApplicationLifetime, logger, serverConfigurationManager, _cleanupGracePeriod)
+ {
+ }
+
+ internal LimitedConcurrencyLibraryScheduler(
+ IHostApplicationLifetime hostApplicationLifetime,
+ ILogger<LimitedConcurrencyLibraryScheduler> logger,
+ IServerConfigurationManager serverConfigurationManager,
+ TimeSpan gracePeriod)
{
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
_serverConfigurationManager = serverConfigurationManager;
+ _gracePeriod = gracePeriod;
+ }
+
+ /// <summary>
+ /// Gets the number of runners the scheduler currently keeps alive.
+ /// </summary>
+ internal int ActiveRunnerCount
+ {
+ get
+ {
+ lock (_taskLock)
+ {
+ return _taskRunners.Count;
+ }
+ }
}
private void ScheduleTaskCleanup()
@@ -68,31 +95,65 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
async Task RunCleanupTask()
{
- _logger.LogDebug("Schedule cleanup task in {CleanupGracePerioid} sec.", CleanupGracePeriod);
- await Task.Delay(TimeSpan.FromSeconds(CleanupGracePeriod)).ConfigureAwait(false);
- if (_disposed)
+ while (true)
{
- _logger.LogDebug("Abort cleaning up, already disposed.");
- return;
- }
+ _logger.LogDebug("Schedule cleanup task in {CleanupGracePeriod}.", _gracePeriod);
+ try
+ {
+ await Task.Delay(_gracePeriod, _disposeTokenSource.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogDebug("Abort cleaning up, already disposed.");
+ return;
+ }
- lock (_taskLock)
- {
- if (_tasks.Reader.Count > 0 || _workCounter > 0)
+ if (_disposed)
{
- _logger.LogDebug("Delay cleanup task, operations still running.");
- // tasks are still there so its still in use. Reschedule cleanup task.
- // we cannot just exit here and rely on the other invoker because there is a considerable timeframe where it could have already ended.
- _cleanupTask = RunCleanupTask();
+ _logger.LogDebug("Abort cleaning up, already disposed.");
return;
}
+
+ CancellationTokenSource[] runners;
+ lock (_taskLock)
+ {
+ if (_tasks.Reader.Count > 0 || _workCounter > 0)
+ {
+ _logger.LogDebug("Delay cleanup task, operations still running.");
+ // tasks are still there so its still in use. Wait another grace period.
+ // we cannot just exit here and rely on the other invoker because there is a considerable timeframe where it could have already ended.
+ continue;
+ }
+
+ runners = [.. _taskRunners.Keys];
+
+ // Retire the runners before they are told to stop: an operation starting while
+ // they wind down must spawn its own instead of counting these towards the fanout.
+ _taskRunners.Clear();
+
+ // Hand the next operation the ability to schedule a cleanup again. Without this
+ // the very first cleanup would be the only one that ever runs.
+ _cleanupTask = null;
+ }
+
+ _logger.LogDebug("Cleanup runners.");
+ await StopRunners(runners).ConfigureAwait(false);
+ return;
}
+ }
+ }
- _logger.LogDebug("Cleanup runners.");
- foreach (var item in _taskRunners.ToArray())
+ private static async Task StopRunners(CancellationTokenSource[] runners)
+ {
+ foreach (var runner in runners)
+ {
+ try
{
- await item.Key.CancelAsync().ConfigureAwait(false);
- _taskRunners.Remove(item.Key);
+ await runner.CancelAsync().ConfigureAwait(false);
+ }
+ catch (ObjectDisposedException)
+ {
+ // The runner already stopped on its own and disposed its stop source.
}
}
}
@@ -127,12 +188,17 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
{
var stopToken = new CancellationTokenSource();
var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping);
+
+ // Keyed on its own stop source, because cancelling that is what reaches the linked
+ // source the runner waits on. Cancellation does not travel the other way.
+ // Started without the runner's own token: a task cancelled before it is scheduled
+ // never runs its body, so it would never take itself out of _taskRunners again.
_taskRunners.Add(
- combinedSource,
+ stopToken,
Task.Factory.StartNew(
ItemWorker,
- (combinedSource, stopToken),
- combinedSource.Token,
+ (stopToken, combinedSource),
+ CancellationToken.None,
TaskCreationOptions.PreferFairness,
TaskScheduler.Default));
}
@@ -145,7 +211,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
_deadlockDetector.Value = stopToken.TaskStop;
try
{
- while (!stopToken.GlobalStop.Token.IsCancellationRequested)
+ while (!stopToken.GlobalStop.IsCancellationRequested)
{
var item = await _tasks.Reader.ReadAsync(stopToken.GlobalStop.Token).ConfigureAwait(false);
try
@@ -162,15 +228,24 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
}
}
}
- catch (OperationCanceledException) when (stopToken.TaskStop.IsCancellationRequested)
+ catch (OperationCanceledException) when (stopToken.GlobalStop.IsCancellationRequested)
{
// thats how you do it, interupt the waiter thread. There is nothing to do here when it was on purpose.
}
+ catch (ChannelClosedException)
+ {
+ // the scheduler was disposed and will not hand out any more work.
+ }
finally
{
_logger.LogDebug("Cleanup Runner'.");
_deadlockDetector.Value = default!;
- _taskRunners.Remove(stopToken.TaskStop);
+
+ lock (_taskLock)
+ {
+ _taskRunners.Remove(stopToken.TaskStop);
+ }
+
stopToken.GlobalStop.Dispose();
stopToken.TaskStop.Dispose();
}
@@ -195,7 +270,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
finally
{
item.Progress.Report(100);
- item.Done.SetResult();
+ item.Done.TrySetResult();
}
}
@@ -285,16 +360,33 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
_disposed = true;
_tasks.Writer.Complete();
- foreach (var item in _taskRunners)
+
+ // Nobody is left to run these, so release whoever is waiting on them.
+ while (_tasks.Reader.TryRead(out var item))
{
- await item.Key.CancelAsync().ConfigureAwait(false);
+ item.Done.TrySetResult();
}
- if (_cleanupTask is not null)
+ CancellationTokenSource[] runners;
+ Task? cleanupTask;
+ lock (_taskLock)
{
- await _cleanupTask.ConfigureAwait(false);
- _cleanupTask?.Dispose();
+ runners = [.. _taskRunners.Keys];
+ _taskRunners.Clear();
+ cleanupTask = _cleanupTask;
}
+
+ await StopRunners(runners).ConfigureAwait(false);
+
+ // Cuts the grace period short instead of holding up shutdown for the rest of it.
+ await _disposeTokenSource.CancelAsync().ConfigureAwait(false);
+
+ if (cleanupTask is not null)
+ {
+ await cleanupTask.ConfigureAwait(false);
+ }
+
+ _disposeTokenSource.Dispose();
}
private class TaskQueueItem
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 06188ad511..73cdf18e91 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -18,7 +18,6 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="BitFaster.Caching" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
</ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index f0c2580e08..a0f4087395 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -442,7 +442,8 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| IsHdr10Plus(state.VideoStream)
|| IsDoviWithHdr10Bl(state.VideoStream)
- || state.VideoStream.VideoRangeType == VideoRangeType.HLG);
+ || state.VideoStream.VideoRangeType == VideoRangeType.HLG
+ || state.VideoStream.VideoRangeType == VideoRangeType.DOVIInvalid);
}
private static bool IsDeinterlaceAvailable(EncodingJobInfo state)
@@ -695,7 +696,11 @@ namespace MediaBrowser.Controller.MediaEncoding
"ogg" or "oga" or "ogv" or "webm" or "webma" => "opus",
"m4a" or "m4b" or "mp4" or "mov" or "mkv" or "mka" => "aac",
"ts" or "avi" or "flv" or "f4v" or "swf" => "mp3",
- _ => inferredCodec
+ // Containers that share their name with the codec they carry.
+ "aac" or "ac3" or "alac" or "dts" or "eac3" or "flac" or "mp2" or "mp3" or "opus" or "truehd" or "vorbis" => inferredCodec,
+ // Anything else - manifests such as m3u8/mpd in particular - names a container that
+ // is not an audio codec. Never hand that name to ffmpeg as an encoder.
+ _ => "aac"
};
}
@@ -1386,7 +1391,8 @@ namespace MediaBrowser.Controller.MediaEncoding
or VideoRangeType.DOVIWithEL
or VideoRangeType.DOVIWithHDR10Plus
or VideoRangeType.DOVIWithELHDR10Plus
- or VideoRangeType.DOVIInvalid;
+ || (rangeType == VideoRangeType.DOVIInvalid
+ && string.Equals(stream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)); // invalid may be hlg now
}
public static bool IsDovi(MediaStream stream)
@@ -1396,7 +1402,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return IsDoviWithHdr10Bl(stream)
|| (rangeType is VideoRangeType.DOVI
or VideoRangeType.DOVIWithHLG
- or VideoRangeType.DOVIWithSDR);
+ or VideoRangeType.DOVIWithSDR
+ or VideoRangeType.DOVIInvalid);
}
public static bool IsHdr10Plus(MediaStream stream)
@@ -1416,7 +1423,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private static DynamicHdrMetadataRemovalPlan ShouldRemoveDynamicHdrMetadata(EncodingJobInfo state)
{
var videoStream = state.VideoStream;
- if (videoStream.VideoRange is not VideoRange.HDR)
+ if (videoStream.VideoRange is not VideoRange.HDR
+ && videoStream.VideoRangeType != VideoRangeType.DOVIInvalid)
{
return DynamicHdrMetadataRemovalPlan.None;
}
diff --git a/MediaBrowser.Controller/Persistence/IItemCountService.cs b/MediaBrowser.Controller/Persistence/IItemCountService.cs
index d57f1fc893..8ddf93e3e0 100644
--- a/MediaBrowser.Controller/Persistence/IItemCountService.cs
+++ b/MediaBrowser.Controller/Persistence/IItemCountService.cs
@@ -80,7 +80,7 @@ public interface IItemCountService
/// Batch-fetches child counts for multiple parent folders.
/// </summary>
/// <param name="parentIds">The list of parent folder IDs.</param>
- /// <param name="userId">The user ID for access filtering.</param>
+ /// <param name="user">The user the counts are for, or null to count without a user's preferences.</param>
/// <returns>Dictionary mapping parent ID to child count.</returns>
- Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId);
+ Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user);
}
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 6060d051a5..f8e0bf4ed9 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -5,13 +5,19 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.Providers
{
public class DirectoryService : IDirectoryService
{
- // TODO make static and switch to FastConcurrentLru.
+ // TODO replace with one shared bounded cache.
+ private const int MaxCachedRecords = 100_000;
+ private const int AccessIntervalMs = 1_000;
+ // Timeout cache if no access for 5 minutes.
+ private const int IdleTimeoutMs = 5 * 60 * 1_000;
+
private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal);
@@ -20,6 +26,12 @@ namespace MediaBrowser.Controller.Providers
private readonly IFileSystem _fileSystem;
+ // ConcurrentDictionary.Count locks the dictionary, so keep an estimated counter.
+ // Concurrent factory runs can overcount and a clear racing an add can undercount,
+ // it only has to be roughly right.
+ private int _recordCount;
+ private long _lastAccess = Environment.TickCount64;
+
public DirectoryService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
@@ -27,20 +39,26 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
+ DropCacheIfIdleOrFull();
+
return _cache.GetOrAdd(
path,
- static (p, fileSystem) =>
+ static (p, state) =>
{
+ FileSystemMetadata[] entries;
try
{
- return fileSystem.GetFileSystemEntries(p).ToArray();
+ entries = state.FileSystem.GetFileSystemEntries(p).ToArray();
}
catch (DirectoryNotFoundException)
{
- return [];
+ entries = [];
}
+
+ Interlocked.Add(ref state.Service._recordCount, entries.Length + 1);
+ return entries;
},
- _fileSystem);
+ (FileSystem: _fileSystem, Service: this));
}
public List<FileSystemMetadata> GetDirectories(string path)
@@ -89,13 +107,18 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata? GetFileSystemEntry(string path)
{
+ DropCacheIfIdleOrFull();
+
if (!_fileCache.TryGetValue(path, out var result))
{
var file = _fileSystem.GetFileSystemInfo(path);
if (file?.Exists ?? false)
{
result = file;
- _fileCache.TryAdd(path, result);
+ if (_fileCache.TryAdd(path, result))
+ {
+ Interlocked.Increment(ref _recordCount);
+ }
}
}
@@ -107,32 +130,96 @@ namespace MediaBrowser.Controller.Providers
public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
{
- if (clearCache)
+ if (clearCache && _filePathCache.TryRemove(path, out var cached))
{
- _filePathCache.TryRemove(path, out _);
+ Interlocked.Add(ref _recordCount, -(cached.Count + 1));
}
+ DropCacheIfIdleOrFull();
+
var filePaths = _filePathCache.GetOrAdd(
path,
- static (p, fileSystem) =>
+ static (p, state) =>
{
+ List<string> filePaths;
try
{
- return fileSystem.GetFilePaths(p).OrderBy(x => x).ToList();
+ filePaths = state.FileSystem.GetFilePaths(p).OrderBy(x => x).ToList();
}
catch (DirectoryNotFoundException)
{
- return [];
+ filePaths = [];
}
+
+ Interlocked.Add(ref state.Service._recordCount, filePaths.Count + 1);
+ return filePaths;
},
- _fileSystem);
+ (FileSystem: _fileSystem, Service: this));
return filePaths;
}
+ public void Invalidate(string path)
+ {
+ Forget(path);
+
+ var parent = Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(parent))
+ {
+ Forget(parent);
+ }
+ }
+
+ public void Move(string source, string destination)
+ {
+ Directory.Move(source, destination);
+
+ Invalidate(source);
+ Invalidate(destination);
+ }
+
public bool IsAccessible(string path)
{
return _fileSystem.GetFileSystemEntryPaths(path).Any();
}
+
+ private void DropCacheIfIdleOrFull()
+ {
+ var nowMs = Environment.TickCount64;
+ var idleMs = nowMs - _lastAccess;
+
+ if (idleMs >= IdleTimeoutMs || _recordCount >= MaxCachedRecords)
+ {
+ _cache.Clear();
+ _fileCache.Clear();
+ _filePathCache.Clear();
+ _recordCount = 0;
+ _lastAccess = nowMs;
+ return;
+ }
+
+ if (idleMs >= AccessIntervalMs)
+ {
+ _lastAccess = nowMs;
+ }
+ }
+
+ private void Forget(string path)
+ {
+ if (_cache.TryRemove(path, out var entries))
+ {
+ Interlocked.Add(ref _recordCount, -(entries.Length + 1));
+ }
+
+ if (_fileCache.TryRemove(path, out _))
+ {
+ Interlocked.Decrement(ref _recordCount);
+ }
+
+ if (_filePathCache.TryRemove(path, out var filePaths))
+ {
+ Interlocked.Add(ref _recordCount, -(filePaths.Count + 1));
+ }
+ }
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 8a3fa33da3..3a943d5f0c 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -23,6 +23,19 @@ namespace MediaBrowser.Controller.Providers
IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
+ /// <summary>
+ /// Forgets what is cached about a path and about the directory containing it.
+ /// </summary>
+ /// <param name="path">The file or directory path that changed.</param>
+ void Invalidate(string path);
+
+ /// <summary>
+ /// Moves a directory and forgets what is cached about both paths.
+ /// </summary>
+ /// <param name="source">The directory to move.</param>
+ /// <param name="destination">The path to move the directory to.</param>
+ void Move(string source, string destination);
+
bool IsAccessible(string path);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index c11c65c334..9acff745b9 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -238,23 +238,26 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Adds the additional user.
/// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="userId">The user identifier.</param>
- void AddAdditionalUser(string sessionId, Guid userId);
+ void AddAdditionalUser(string controllingSessionId, string sessionId, Guid userId);
/// <summary>
/// Removes the additional user.
/// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="userId">The user identifier.</param>
- void RemoveAdditionalUser(string sessionId, Guid userId);
+ void RemoveAdditionalUser(string controllingSessionId, string sessionId, Guid userId);
/// <summary>
/// Reports the now viewing item.
/// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="itemId">The item identifier.</param>
- void ReportNowViewingItem(string sessionId, string itemId);
+ void ReportNowViewingItem(string controllingSessionId, string sessionId, string itemId);
/// <summary>
/// Authenticates the new session.
@@ -268,9 +271,10 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Reports the capabilities.
/// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="capabilities">The capabilities.</param>
- void ReportCapabilities(string sessionId, ClientCapabilities capabilities);
+ void ReportCapabilities(string controllingSessionId, string sessionId, ClientCapabilities capabilities);
/// <summary>
/// Reports the transcoding information.
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
index eb38eeb503..f4fab29800 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -501,7 +501,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
{
// Client, that was buffering, resumed playback but did not update others in time.
delayTicks = context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond;
- delayTicks = Math.Max(delayTicks, context.DefaultPing);
+ delayTicks = Math.Max(delayTicks, TimeSpan.FromMilliseconds(context.DefaultPing).Ticks);
context.LastActivity = currentTime.AddTicks(delayTicks);
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
index 9326864d78..258b92e4d9 100644
--- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -157,7 +157,10 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// </summary>
public void RestoreSortedPlaylist()
{
- if (PlayingItemIndex != NoPlayingItemIndex)
+ // The shuffled playlist is only populated while the shuffle mode is active, so there is
+ // nothing to map back when the playlist is already sorted. Guarding on its contents keeps
+ // a redundant request for the sorted mode from indexing an empty list.
+ if (PlayingItemIndex != NoPlayingItemIndex && _shuffledPlaylist.Count > 0)
{
var playingItem = _shuffledPlaylist[PlayingItemIndex];
PlayingItemIndex = _sortedPlaylist.IndexOf(playingItem);