< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.IO.LibraryMonitor
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/IO/LibraryMonitor.cs
Line coverage
20%
Covered lines: 40
Uncovered lines: 153
Coverable lines: 193
Total lines: 491
Line coverage: 20.7%
Branch coverage
10%
Covered branches: 7
Total branches: 70
Branch coverage: 10%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/25/2025 - 12:09:58 AM Line coverage: 21% (40/190) Branch coverage: 10.2% (7/68) Total lines: 48512/29/2025 - 12:13:19 AM Line coverage: 22.1% (42/190) Branch coverage: 11.7% (8/68) Total lines: 48512/30/2025 - 12:12:35 AM Line coverage: 21% (40/190) Branch coverage: 10.2% (7/68) Total lines: 4851/19/2026 - 12:13:54 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 491 10/25/2025 - 12:09:58 AM Line coverage: 21% (40/190) Branch coverage: 10.2% (7/68) Total lines: 48512/29/2025 - 12:13:19 AM Line coverage: 22.1% (42/190) Branch coverage: 11.7% (8/68) Total lines: 48512/30/2025 - 12:12:35 AM Line coverage: 21% (40/190) Branch coverage: 10.2% (7/68) Total lines: 4851/19/2026 - 12:13:54 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 491

Metrics

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/IO/LibraryMonitor.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.IO;
 5using System.Linq;
 6using System.Threading.Tasks;
 7using Emby.Server.Implementations.Library;
 8using MediaBrowser.Controller.Configuration;
 9using MediaBrowser.Controller.Entities;
 10using MediaBrowser.Controller.Library;
 11using MediaBrowser.Model.IO;
 12using Microsoft.Extensions.Hosting;
 13using Microsoft.Extensions.Logging;
 14
 15namespace Emby.Server.Implementations.IO
 16{
 17    /// <inheritdoc cref="ILibraryMonitor" />
 18    public sealed class LibraryMonitor : ILibraryMonitor, IDisposable
 19    {
 20        private readonly ILogger<LibraryMonitor> _logger;
 21        private readonly ILibraryManager _libraryManager;
 22        private readonly IServerConfigurationManager _configurationManager;
 23        private readonly IFileSystem _fileSystem;
 24
 25        /// <summary>
 26        /// The file system watchers.
 27        /// </summary>
 2128        private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.Ordina
 29
 30        /// <summary>
 31        /// The affected paths.
 32        /// </summary>
 2133        private readonly List<FileRefresher> _activeRefreshers = [];
 34
 35        /// <summary>
 36        /// A dynamic list of paths that should be ignored.  Added to during our own file system modifications.
 37        /// </summary>
 2138        private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
 39
 40        private bool _disposed;
 41
 42        /// <summary>
 43        /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
 44        /// </summary>
 45        /// <param name="logger">The logger.</param>
 46        /// <param name="libraryManager">The library manager.</param>
 47        /// <param name="configurationManager">The configuration manager.</param>
 48        /// <param name="fileSystem">The filesystem.</param>
 49        /// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
 50        public LibraryMonitor(
 51            ILogger<LibraryMonitor> logger,
 52            ILibraryManager libraryManager,
 53            IServerConfigurationManager configurationManager,
 54            IFileSystem fileSystem,
 55            IHostApplicationLifetime appLifetime)
 56        {
 2157            _libraryManager = libraryManager;
 2158            _logger = logger;
 2159            _configurationManager = configurationManager;
 2160            _fileSystem = fileSystem;
 61
 2162            appLifetime.ApplicationStarted.Register(Start);
 2163        }
 64
 65        /// <inheritdoc />
 66        public void ReportFileSystemChangeBeginning(string path)
 67        {
 068            ArgumentException.ThrowIfNullOrEmpty(path);
 69
 070            _tempIgnoredPaths[path] = path;
 071        }
 72
 73        /// <inheritdoc />
 74        public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
 75        {
 76            ArgumentException.ThrowIfNullOrEmpty(path);
 77
 78            // This is an arbitrary amount of time, but delay it because file system writes often trigger events long af
 79            // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
 80            // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, o
 81            await Task.Delay(45000).ConfigureAwait(false);
 82
 83            _tempIgnoredPaths.TryRemove(path, out _);
 84
 85            if (refreshPath)
 86            {
 87                try
 88                {
 89                    ReportFileSystemChanged(path);
 90                }
 91                catch (Exception ex)
 92                {
 93                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
 94                }
 95            }
 96        }
 97
 98        private bool IsLibraryMonitorEnabled(BaseItem item)
 99        {
 42100            if (item is BasePluginFolder)
 101            {
 42102                return false;
 103            }
 104
 0105            var options = _libraryManager.GetLibraryOptions(item);
 106
 0107            return options is not null && options.EnableRealtimeMonitor;
 108        }
 109
 110        /// <inheritdoc />
 111        public void Start()
 112        {
 42113            _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
 42114            _libraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
 115
 42116            var pathsToWatch = new List<string>();
 117
 42118            var paths = _libraryManager
 42119                .RootFolder
 42120                .Children
 42121                .Where(IsLibraryMonitorEnabled)
 42122                .OfType<Folder>()
 42123                .SelectMany(f => f.PhysicalLocations)
 42124                .Distinct()
 42125                .Order();
 126
 84127            foreach (var path in paths)
 128            {
 0129                if (!ContainsParentFolder(pathsToWatch, path))
 130                {
 0131                    pathsToWatch.Add(path);
 132                }
 133            }
 134
 84135            foreach (var path in pathsToWatch)
 136            {
 0137                StartWatchingPath(path);
 138            }
 42139        }
 140
 141        private void StartWatching(BaseItem item)
 142        {
 0143            if (IsLibraryMonitorEnabled(item))
 144            {
 0145                StartWatchingPath(item.Path);
 146            }
 0147        }
 148
 149        /// <summary>
 150        /// Handles the ItemRemoved event of the LibraryManager control.
 151        /// </summary>
 152        /// <param name="sender">The source of the event.</param>
 153        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 154        private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
 155        {
 0156            if (e.Parent is AggregateFolder)
 157            {
 0158                StopWatchingPath(e.Item.Path);
 159            }
 0160        }
 161
 162        /// <summary>
 163        /// Handles the ItemAdded event of the LibraryManager control.
 164        /// </summary>
 165        /// <param name="sender">The source of the event.</param>
 166        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 167        private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
 168        {
 0169            if (e.Parent is AggregateFolder)
 170            {
 0171                StartWatching(e.Item);
 172            }
 0173        }
 174
 175        /// <summary>
 176        /// Examine a list of strings assumed to be file paths to see if it contains a parent of
 177        /// the provided path.
 178        /// </summary>
 179        /// <param name="lst">The LST.</param>
 180        /// <param name="path">The path.</param>
 181        /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
 182        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
 183        private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
 184        {
 0185            if (path.IsEmpty)
 186            {
 0187                throw new ArgumentException("Path can't be empty", nameof(path));
 188            }
 189
 0190            path = path.TrimEnd(Path.DirectorySeparatorChar);
 191
 0192            foreach (var str in lst)
 193            {
 194                // this should be a little quicker than examining each actual parent folder...
 0195                var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
 196
 0197                if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
 0198                    || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.Dir
 199                {
 0200                    return true;
 201                }
 202            }
 203
 0204            return false;
 0205        }
 206
 207        /// <summary>
 208        /// Starts the watching path.
 209        /// </summary>
 210        /// <param name="path">The path.</param>
 211        private void StartWatchingPath(string path)
 212        {
 0213            if (!Directory.Exists(path))
 214            {
 215                // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
 0216                _logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
 0217                return;
 218            }
 219
 220            // Already being watched
 0221            if (_fileSystemWatchers.ContainsKey(path))
 222            {
 0223                return;
 224            }
 225
 226            // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do t
 0227            Task.Run(() =>
 0228            {
 0229                try
 0230                {
 0231                    var newWatcher = new FileSystemWatcher(path, "*")
 0232                    {
 0233                        IncludeSubdirectories = true,
 0234                        InternalBufferSize = 65536,
 0235                        NotifyFilter = NotifyFilters.CreationTime |
 0236                                       NotifyFilters.DirectoryName |
 0237                                       NotifyFilters.FileName |
 0238                                       NotifyFilters.LastWrite |
 0239                                       NotifyFilters.Size |
 0240                                       NotifyFilters.Attributes
 0241                    };
 0242
 0243                    newWatcher.Created += OnWatcherChanged;
 0244                    newWatcher.Deleted += OnWatcherChanged;
 0245                    newWatcher.Renamed += OnWatcherChanged;
 0246                    newWatcher.Changed += OnWatcherChanged;
 0247                    newWatcher.Error += OnWatcherError;
 0248
 0249                    if (_fileSystemWatchers.TryAdd(path, newWatcher))
 0250                    {
 0251                        newWatcher.EnableRaisingEvents = true;
 0252                        _logger.LogInformation("Watching directory {Path}", path);
 0253                    }
 0254                    else
 0255                    {
 0256                        DisposeWatcher(newWatcher, false);
 0257                    }
 0258                }
 0259                catch (Exception ex)
 0260                {
 0261                    _logger.LogError(ex, "Error watching path: {Path}", path);
 0262                }
 0263            });
 0264        }
 265
 266        /// <summary>
 267        /// Stops the watching path.
 268        /// </summary>
 269        /// <param name="path">The path.</param>
 270        private void StopWatchingPath(string path)
 271        {
 0272            if (_fileSystemWatchers.TryGetValue(path, out var watcher))
 273            {
 0274                DisposeWatcher(watcher, true);
 275            }
 0276        }
 277
 278        /// <summary>
 279        /// Disposes the watcher.
 280        /// </summary>
 281        private void DisposeWatcher(FileSystemWatcher watcher, bool removeFromList)
 282        {
 283            try
 284            {
 0285                using (watcher)
 286                {
 0287                    _logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
 288
 0289                    watcher.Created -= OnWatcherChanged;
 0290                    watcher.Deleted -= OnWatcherChanged;
 0291                    watcher.Renamed -= OnWatcherChanged;
 0292                    watcher.Changed -= OnWatcherChanged;
 0293                    watcher.Error -= OnWatcherError;
 294
 0295                    watcher.EnableRaisingEvents = false;
 0296                }
 297            }
 298            finally
 299            {
 0300                if (removeFromList)
 301                {
 0302                    _fileSystemWatchers.TryRemove(watcher.Path, out _);
 303                }
 0304            }
 0305        }
 306
 307        /// <summary>
 308        /// Handles the Error event of the watcher control.
 309        /// </summary>
 310        /// <param name="sender">The source of the event.</param>
 311        /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
 312        private void OnWatcherError(object sender, ErrorEventArgs e)
 313        {
 0314            var ex = e.GetException();
 0315            var dw = (FileSystemWatcher)sender;
 316
 0317            if (ex is UnauthorizedAccessException unauthorizedAccessException)
 318            {
 0319                _logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path)
 0320                return;
 321            }
 322
 0323            _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
 324
 0325            DisposeWatcher(dw, true);
 0326        }
 327
 328        /// <summary>
 329        /// Handles the Changed event of the watcher control.
 330        /// </summary>
 331        /// <param name="sender">The source of the event.</param>
 332        /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
 333        private void OnWatcherChanged(object sender, FileSystemEventArgs e)
 334        {
 335            try
 336            {
 0337                ReportFileSystemChanged(e.FullPath);
 0338            }
 0339            catch (Exception ex)
 340            {
 0341                _logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
 0342            }
 0343        }
 344
 345        /// <inheritdoc />
 346        public void ReportFileSystemChanged(string path)
 347        {
 0348            ArgumentException.ThrowIfNullOrEmpty(path);
 349
 0350            if (IgnorePatterns.ShouldIgnore(path))
 351            {
 0352                return;
 353            }
 354
 0355            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 0356            if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
 357            {
 0358                return;
 359            }
 360
 361            // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
 0362            foreach (var i in _tempIgnoredPaths.Keys)
 363            {
 0364                if (_fileSystem.AreEqual(i, path)
 0365                    || _fileSystem.ContainsSubPath(i, path))
 366                {
 0367                    _logger.LogDebug("Ignoring change to {Path}", path);
 0368                    return;
 369                }
 370
 371                // Go up a level
 0372                var parent = Path.GetDirectoryName(i);
 0373                if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
 374                {
 0375                    _logger.LogDebug("Ignoring change to {Path}", path);
 0376                    return;
 377                }
 378            }
 379
 0380            CreateRefresher(path);
 0381        }
 382
 383        private void CreateRefresher(string path)
 384        {
 0385            var parentPath = Path.GetDirectoryName(path);
 386
 0387            lock (_activeRefreshers)
 388            {
 0389                foreach (var refresher in _activeRefreshers)
 390                {
 391                    // Path is already being refreshed
 0392                    if (_fileSystem.AreEqual(path, refresher.Path))
 393                    {
 0394                        refresher.RestartTimer();
 0395                        return;
 396                    }
 397
 398                    // Parent folder is already being refreshed
 0399                    if (_fileSystem.ContainsSubPath(refresher.Path, path))
 400                    {
 0401                        refresher.AddPath(path);
 0402                        return;
 403                    }
 404
 405                    // New path is a parent
 0406                    if (_fileSystem.ContainsSubPath(path, refresher.Path))
 407                    {
 0408                        refresher.ResetPath(path, null);
 0409                        return;
 410                    }
 411
 412                    // They are siblings. Rebase the refresher to the parent folder.
 0413                    if (parentPath is not null
 0414                        && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
 415                    {
 0416                        refresher.ResetPath(parentPath, path);
 0417                        return;
 418                    }
 419                }
 420
 0421                var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
 0422                newRefresher.Completed += OnNewRefresherCompleted;
 0423                _activeRefreshers.Add(newRefresher);
 0424            }
 0425        }
 426
 427        private void OnNewRefresherCompleted(object? sender, EventArgs e)
 428        {
 0429            if (sender is null)
 430            {
 0431                return;
 432            }
 433
 0434            var refresher = (FileRefresher)sender;
 0435            DisposeRefresher(refresher);
 0436        }
 437
 438        /// <summary>
 439        /// Stops this instance.
 440        /// </summary>
 441        public void Stop()
 442        {
 45443            _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
 45444            _libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
 445
 90446            foreach (var watcher in _fileSystemWatchers.Values.ToList())
 447            {
 0448                DisposeWatcher(watcher, false);
 449            }
 450
 45451            _fileSystemWatchers.Clear();
 45452            DisposeRefreshers();
 45453        }
 454
 455        private void DisposeRefresher(FileRefresher refresher)
 456        {
 0457            lock (_activeRefreshers)
 458            {
 0459                refresher.Completed -= OnNewRefresherCompleted;
 0460                refresher.Dispose();
 0461                _activeRefreshers.Remove(refresher);
 0462            }
 0463        }
 464
 465        private void DisposeRefreshers()
 466        {
 45467            lock (_activeRefreshers)
 468            {
 90469                foreach (var refresher in _activeRefreshers)
 470                {
 0471                    refresher.Completed -= OnNewRefresherCompleted;
 0472                    refresher.Dispose();
 473                }
 474
 45475                _activeRefreshers.Clear();
 45476            }
 45477        }
 478
 479        /// <inheritdoc />
 480        public void Dispose()
 481        {
 21482            if (_disposed)
 483            {
 0484                return;
 485            }
 486
 21487            Stop();
 21488            _disposed = true;
 21489        }
 490    }
 491}