< 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
22%
Covered lines: 49
Uncovered lines: 168
Coverable lines: 217
Total lines: 529
Line coverage: 22.5%
Branch coverage
14%
Covered branches: 11
Total branches: 76
Branch coverage: 14.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 20.4% (42/205) Branch coverage: 9.7% (7/72) Total lines: 49610/29/2026 - 12:17:11 AM Line coverage: 22.5% (49/217) Branch coverage: 14.4% (11/76) Total lines: 529 5/16/2026 - 12:15:55 AM Line coverage: 20.4% (42/205) Branch coverage: 9.7% (7/72) Total lines: 49610/29/2026 - 12:17:11 AM Line coverage: 22.5% (49/217) Branch coverage: 14.4% (11/76) Total lines: 529

Coverage delta

Coverage delta 5 -5

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;
 7using System.Threading.Tasks;
 8using Emby.Server.Implementations.Library;
 9using MediaBrowser.Controller.Configuration;
 10using MediaBrowser.Controller.Entities;
 11using MediaBrowser.Controller.Library;
 12using MediaBrowser.Controller.Providers;
 13using MediaBrowser.Model.IO;
 14using Microsoft.Extensions.Hosting;
 15using Microsoft.Extensions.Logging;
 16
 17namespace Emby.Server.Implementations.IO
 18{
 19    /// <inheritdoc cref="ILibraryMonitor" />
 20    public sealed class LibraryMonitor : ILibraryMonitor, IDisposable
 21    {
 22        private readonly ILogger<LibraryMonitor> _logger;
 23        private readonly ILibraryManager _libraryManager;
 24        private readonly IServerConfigurationManager _configurationManager;
 25        private readonly IFileSystem _fileSystem;
 26        private readonly IDirectoryService _directoryService;
 27        private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
 28
 29        /// <summary>
 30        /// The file system watchers.
 31        /// </summary>
 2732        private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.Ordina
 33
 34        /// <summary>
 35        /// The affected paths.
 36        /// </summary>
 2737        private readonly List<FileRefresher> _activeRefreshers = [];
 38
 39        /// <summary>
 40        /// A dynamic list of paths that should be ignored.  Added to during our own file system modifications.
 41        /// </summary>
 2742        private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
 43
 44        /// <summary>
 45        /// Incremented by every <see cref="Stop"/> so watchers still being created on a background
 46        /// task can tell that the sweep they should have been caught by has already run.
 47        /// </summary>
 48        private int _watcherGeneration;
 49
 50        private bool _disposed;
 51
 52        /// <summary>
 53        /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
 54        /// </summary>
 55        /// <param name="logger">The logger.</param>
 56        /// <param name="libraryManager">The library manager.</param>
 57        /// <param name="configurationManager">The configuration manager.</param>
 58        /// <param name="fileSystem">The filesystem.</param>
 59        /// <param name="directoryService">The directory service.</param>
 60        /// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
 61        /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
 62        public LibraryMonitor(
 63            ILogger<LibraryMonitor> logger,
 64            ILibraryManager libraryManager,
 65            IServerConfigurationManager configurationManager,
 66            IFileSystem fileSystem,
 67            IDirectoryService directoryService,
 68            IHostApplicationLifetime appLifetime,
 69            DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
 70        {
 2771            _libraryManager = libraryManager;
 2772            _logger = logger;
 2773            _configurationManager = configurationManager;
 2774            _fileSystem = fileSystem;
 2775            _directoryService = directoryService;
 2776            _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
 77
 2778            appLifetime.ApplicationStarted.Register(Start);
 2779            appLifetime.ApplicationStopping.Register(Dispose);
 2780        }
 81
 82        /// <inheritdoc />
 83        public void ReportFileSystemChangeBeginning(string path)
 84        {
 085            ArgumentException.ThrowIfNullOrEmpty(path);
 86
 087            _tempIgnoredPaths[path] = path;
 088        }
 89
 90        /// <inheritdoc />
 91        public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
 92        {
 093            ArgumentException.ThrowIfNullOrEmpty(path);
 94
 95            // This is an arbitrary amount of time, but delay it because file system writes often trigger events long af
 96            // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
 97            // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, o
 098            await Task.Delay(45000).ConfigureAwait(false);
 99
 0100            _tempIgnoredPaths.TryRemove(path, out _);
 101
 0102            if (refreshPath)
 103            {
 104                try
 105                {
 0106                    ReportFileSystemChanged(path);
 0107                }
 0108                catch (Exception ex)
 109                {
 0110                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
 0111                }
 112            }
 0113        }
 114
 115        private bool IsLibraryMonitorEnabled(BaseItem item)
 116        {
 53117            if (item is BasePluginFolder)
 118            {
 53119                return false;
 120            }
 121
 0122            var options = _libraryManager.GetLibraryOptions(item);
 123
 0124            return options is not null && options.EnableRealtimeMonitor;
 125        }
 126
 127        /// <inheritdoc />
 128        public void Start()
 129        {
 56130            if (_disposed)
 131            {
 3132                return;
 133            }
 134
 53135            _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
 53136            _libraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
 137
 53138            var pathsToWatch = new List<string>();
 139
 53140            var paths = _libraryManager
 53141                .RootFolder
 53142                .Children
 53143                .Where(IsLibraryMonitorEnabled)
 53144                .OfType<Folder>()
 53145                .SelectMany(f => f.PhysicalLocations)
 53146                .Distinct()
 53147                .Order();
 148
 106149            foreach (var path in paths)
 150            {
 0151                if (!ContainsParentFolder(pathsToWatch, path))
 152                {
 0153                    pathsToWatch.Add(path);
 154                }
 155            }
 156
 106157            foreach (var path in pathsToWatch)
 158            {
 0159                StartWatchingPath(path);
 160            }
 53161        }
 162
 163        private void StartWatching(BaseItem item)
 164        {
 0165            if (IsLibraryMonitorEnabled(item))
 166            {
 0167                StartWatchingPath(item.Path);
 168            }
 0169        }
 170
 171        /// <summary>
 172        /// Handles the ItemRemoved event of the LibraryManager control.
 173        /// </summary>
 174        /// <param name="sender">The source of the event.</param>
 175        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 176        private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
 177        {
 0178            if (e.Parent is AggregateFolder)
 179            {
 0180                StopWatchingPath(e.Item.Path);
 181            }
 0182        }
 183
 184        /// <summary>
 185        /// Handles the ItemAdded event of the LibraryManager control.
 186        /// </summary>
 187        /// <param name="sender">The source of the event.</param>
 188        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 189        private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
 190        {
 7191            if (e.Parent is AggregateFolder)
 192            {
 0193                StartWatching(e.Item);
 194            }
 7195        }
 196
 197        /// <summary>
 198        /// Examine a list of strings assumed to be file paths to see if it contains a parent of
 199        /// the provided path.
 200        /// </summary>
 201        /// <param name="lst">The LST.</param>
 202        /// <param name="path">The path.</param>
 203        /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
 204        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
 205        private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
 206        {
 0207            if (path.IsEmpty)
 208            {
 0209                throw new ArgumentException("Path can't be empty", nameof(path));
 210            }
 211
 0212            path = path.TrimEnd(Path.DirectorySeparatorChar);
 213
 0214            foreach (var str in lst)
 215            {
 216                // this should be a little quicker than examining each actual parent folder...
 0217                var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
 218
 0219                if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
 0220                    || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.Dir
 221                {
 0222                    return true;
 223                }
 224            }
 225
 0226            return false;
 0227        }
 228
 229        /// <summary>
 230        /// Starts the watching path.
 231        /// </summary>
 232        /// <param name="path">The path.</param>
 233        private void StartWatchingPath(string path)
 234        {
 0235            if (!Directory.Exists(path))
 236            {
 237                // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
 0238                _logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
 0239                return;
 240            }
 241
 242            // Already being watched
 0243            if (_fileSystemWatchers.ContainsKey(path))
 244            {
 0245                return;
 246            }
 247
 0248            var generation = Volatile.Read(ref _watcherGeneration);
 249
 250            // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do t
 0251            Task.Run(() =>
 0252            {
 0253                try
 0254                {
 0255                    var newWatcher = new FileSystemWatcher(path, "*")
 0256                    {
 0257                        IncludeSubdirectories = true,
 0258                        InternalBufferSize = 65536,
 0259                        NotifyFilter = NotifyFilters.CreationTime |
 0260                                       NotifyFilters.DirectoryName |
 0261                                       NotifyFilters.FileName |
 0262                                       NotifyFilters.LastWrite |
 0263                                       NotifyFilters.Size |
 0264                                       NotifyFilters.Attributes
 0265                    };
 0266
 0267                    newWatcher.Created += OnWatcherChanged;
 0268                    newWatcher.Deleted += OnWatcherChanged;
 0269                    newWatcher.Renamed += OnWatcherChanged;
 0270                    newWatcher.Changed += OnWatcherChanged;
 0271                    newWatcher.Error += OnWatcherError;
 0272
 0273                    if (_disposed || Volatile.Read(ref _watcherGeneration) != generation)
 0274                    {
 0275                        DisposeWatcher(newWatcher, false);
 0276                    }
 0277                    else if (_fileSystemWatchers.TryAdd(path, newWatcher))
 0278                    {
 0279                        newWatcher.EnableRaisingEvents = true;
 0280                        _logger.LogInformation("Watching directory {Path}", path);
 0281                    }
 0282                    else
 0283                    {
 0284                        DisposeWatcher(newWatcher, false);
 0285                    }
 0286                }
 0287                catch (Exception ex)
 0288                {
 0289                    _logger.LogError(ex, "Error watching path: {Path}", path);
 0290                }
 0291            });
 0292        }
 293
 294        /// <summary>
 295        /// Stops the watching path.
 296        /// </summary>
 297        /// <param name="path">The path.</param>
 298        private void StopWatchingPath(string path)
 299        {
 0300            if (_fileSystemWatchers.TryGetValue(path, out var watcher))
 301            {
 0302                DisposeWatcher(watcher, true);
 303            }
 0304        }
 305
 306        /// <summary>
 307        /// Disposes the watcher.
 308        /// </summary>
 309        private void DisposeWatcher(FileSystemWatcher watcher, bool removeFromList)
 310        {
 311            try
 312            {
 0313                using (watcher)
 314                {
 0315                    _logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
 316
 0317                    watcher.Created -= OnWatcherChanged;
 0318                    watcher.Deleted -= OnWatcherChanged;
 0319                    watcher.Renamed -= OnWatcherChanged;
 0320                    watcher.Changed -= OnWatcherChanged;
 0321                    watcher.Error -= OnWatcherError;
 322
 0323                    watcher.EnableRaisingEvents = false;
 0324                }
 325            }
 326            finally
 327            {
 0328                if (removeFromList)
 329                {
 0330                    _fileSystemWatchers.TryRemove(watcher.Path, out _);
 331                }
 0332            }
 0333        }
 334
 335        /// <summary>
 336        /// Handles the Error event of the watcher control.
 337        /// </summary>
 338        /// <param name="sender">The source of the event.</param>
 339        /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
 340        private void OnWatcherError(object sender, ErrorEventArgs e)
 341        {
 0342            var ex = e.GetException();
 0343            var dw = (FileSystemWatcher)sender;
 344
 0345            if (ex is UnauthorizedAccessException unauthorizedAccessException)
 346            {
 0347                _logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path)
 0348                return;
 349            }
 350
 0351            _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
 352
 0353            DisposeWatcher(dw, true);
 0354        }
 355
 356        /// <summary>
 357        /// Handles the Changed event of the watcher control.
 358        /// </summary>
 359        /// <param name="sender">The source of the event.</param>
 360        /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
 361        private void OnWatcherChanged(object sender, FileSystemEventArgs e)
 362        {
 363            try
 364            {
 0365                ReportFileSystemChanged(e.FullPath);
 0366            }
 0367            catch (Exception ex)
 368            {
 0369                _logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
 0370            }
 0371        }
 372
 373        /// <inheritdoc />
 374        public void ReportFileSystemChanged(string path)
 375        {
 0376            ArgumentException.ThrowIfNullOrEmpty(path);
 377
 0378            if (_disposed)
 379            {
 0380                return;
 381            }
 382
 0383            if (IgnorePatterns.ShouldIgnore(path))
 384            {
 0385                return;
 386            }
 387
 0388            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 0389            if (_dotIgnoreIgnoreRule.ShouldIgnore(fileInfo, null))
 390            {
 0391                return;
 392            }
 393
 0394            _directoryService.Invalidate(path);
 395
 396            // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
 0397            foreach (var i in _tempIgnoredPaths.Keys)
 398            {
 0399                if (_fileSystem.AreEqual(i, path)
 0400                    || _fileSystem.ContainsSubPath(i, path))
 401                {
 0402                    _logger.LogDebug("Ignoring change to {Path}", path);
 0403                    return;
 404                }
 405
 406                // Go up a level
 0407                var parent = Path.GetDirectoryName(i);
 0408                if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
 409                {
 0410                    _logger.LogDebug("Ignoring change to {Path}", path);
 0411                    return;
 412                }
 413            }
 414
 0415            CreateRefresher(path);
 0416        }
 417
 418        private void CreateRefresher(string path)
 419        {
 0420            var parentPath = Path.GetDirectoryName(path);
 421
 0422            lock (_activeRefreshers)
 423            {
 0424                foreach (var refresher in _activeRefreshers)
 425                {
 426                    // Path is already being refreshed
 0427                    if (_fileSystem.AreEqual(path, refresher.Path))
 428                    {
 0429                        refresher.RestartTimer();
 0430                        return;
 431                    }
 432
 433                    // Parent folder is already being refreshed
 0434                    if (_fileSystem.ContainsSubPath(refresher.Path, path))
 435                    {
 0436                        refresher.AddPath(path);
 0437                        return;
 438                    }
 439
 440                    // New path is a parent
 0441                    if (_fileSystem.ContainsSubPath(path, refresher.Path))
 442                    {
 0443                        refresher.ResetPath(path, null);
 0444                        return;
 445                    }
 446
 447                    // They are siblings. Rebase the refresher to the parent folder.
 0448                    if (parentPath is not null
 0449                        && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
 450                    {
 0451                        refresher.ResetPath(parentPath, path);
 0452                        return;
 453                    }
 454                }
 455
 0456                var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
 0457                newRefresher.Completed += OnNewRefresherCompleted;
 0458                _activeRefreshers.Add(newRefresher);
 0459            }
 0460        }
 461
 462        private void OnNewRefresherCompleted(object? sender, EventArgs e)
 463        {
 0464            if (sender is null)
 465            {
 0466                return;
 467            }
 468
 0469            var refresher = (FileRefresher)sender;
 0470            DisposeRefresher(refresher);
 0471        }
 472
 473        /// <summary>
 474        /// Stops this instance.
 475        /// </summary>
 476        public void Stop()
 477        {
 61478            Interlocked.Increment(ref _watcherGeneration);
 479
 61480            _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
 61481            _libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
 482
 122483            foreach (var watcher in _fileSystemWatchers.Values.ToList())
 484            {
 0485                DisposeWatcher(watcher, false);
 486            }
 487
 61488            _fileSystemWatchers.Clear();
 61489            DisposeRefreshers();
 61490        }
 491
 492        private void DisposeRefresher(FileRefresher refresher)
 493        {
 0494            lock (_activeRefreshers)
 495            {
 0496                refresher.Completed -= OnNewRefresherCompleted;
 0497                refresher.Dispose();
 0498                _activeRefreshers.Remove(refresher);
 0499            }
 0500        }
 501
 502        private void DisposeRefreshers()
 503        {
 61504            lock (_activeRefreshers)
 505            {
 122506                foreach (var refresher in _activeRefreshers)
 507                {
 0508                    refresher.Completed -= OnNewRefresherCompleted;
 0509                    refresher.Dispose();
 510                }
 511
 61512                _activeRefreshers.Clear();
 61513            }
 61514        }
 515
 516        /// <inheritdoc />
 517        public void Dispose()
 518        {
 54519            if (_disposed)
 520            {
 27521                return;
 522            }
 523
 524            // Set before stopping so anything racing us stops handing out new work.
 27525            _disposed = true;
 27526            Stop();
 27527        }
 528    }
 529}