< 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: 42
Uncovered lines: 163
Coverable lines: 205
Total lines: 496
Line coverage: 20.4%
Branch coverage
9%
Covered branches: 7
Total branches: 72
Branch coverage: 9.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 4913/15/2026 - 12:13:57 AM Line coverage: 21.7% (42/193) Branch coverage: 11.4% (8/70) Total lines: 4913/16/2026 - 12:14:00 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 4914/6/2026 - 12:13:55 AM Line coverage: 21.7% (42/193) Branch coverage: 11.4% (8/70) Total lines: 4914/7/2026 - 12:14:03 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 4914/14/2026 - 12:13:23 AM Line coverage: 21.1% (41/194) Branch coverage: 10% (7/70) Total lines: 4924/19/2026 - 12:14:27 AM Line coverage: 20% (41/204) Branch coverage: 9.7% (7/72) Total lines: 4924/23/2026 - 12:14:52 AM Line coverage: 21% (43/204) Branch coverage: 11.1% (8/72) Total lines: 4924/24/2026 - 12:14:24 AM Line coverage: 20% (41/204) Branch coverage: 9.7% (7/72) Total lines: 4925/5/2026 - 12:15:44 AM Line coverage: 20.4% (42/205) Branch coverage: 9.7% (7/72) Total lines: 496 1/23/2026 - 12:11:06 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 4913/15/2026 - 12:13:57 AM Line coverage: 21.7% (42/193) Branch coverage: 11.4% (8/70) Total lines: 4913/16/2026 - 12:14:00 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 4914/6/2026 - 12:13:55 AM Line coverage: 21.7% (42/193) Branch coverage: 11.4% (8/70) Total lines: 4914/7/2026 - 12:14:03 AM Line coverage: 20.7% (40/193) Branch coverage: 10% (7/70) Total lines: 4914/14/2026 - 12:13:23 AM Line coverage: 21.1% (41/194) Branch coverage: 10% (7/70) Total lines: 4924/19/2026 - 12:14:27 AM Line coverage: 20% (41/204) Branch coverage: 9.7% (7/72) Total lines: 4924/23/2026 - 12:14:52 AM Line coverage: 21% (43/204) Branch coverage: 11.1% (8/72) Total lines: 4924/24/2026 - 12:14:24 AM Line coverage: 20% (41/204) Branch coverage: 9.7% (7/72) Total lines: 4925/5/2026 - 12:15:44 AM Line coverage: 20.4% (42/205) Branch coverage: 9.7% (7/72) Total lines: 496

Coverage delta

Coverage delta 2 -2

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        private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
 25
 26        /// <summary>
 27        /// The file system watchers.
 28        /// </summary>
 2129        private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.Ordina
 30
 31        /// <summary>
 32        /// The affected paths.
 33        /// </summary>
 2134        private readonly List<FileRefresher> _activeRefreshers = [];
 35
 36        /// <summary>
 37        /// A dynamic list of paths that should be ignored.  Added to during our own file system modifications.
 38        /// </summary>
 2139        private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
 40
 41        private bool _disposed;
 42
 43        /// <summary>
 44        /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
 45        /// </summary>
 46        /// <param name="logger">The logger.</param>
 47        /// <param name="libraryManager">The library manager.</param>
 48        /// <param name="configurationManager">The configuration manager.</param>
 49        /// <param name="fileSystem">The filesystem.</param>
 50        /// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
 51        /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
 52        public LibraryMonitor(
 53            ILogger<LibraryMonitor> logger,
 54            ILibraryManager libraryManager,
 55            IServerConfigurationManager configurationManager,
 56            IFileSystem fileSystem,
 57            IHostApplicationLifetime appLifetime,
 58            DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
 59        {
 2160            _libraryManager = libraryManager;
 2161            _logger = logger;
 2162            _configurationManager = configurationManager;
 2163            _fileSystem = fileSystem;
 2164            _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
 65
 2166            appLifetime.ApplicationStarted.Register(Start);
 2167            appLifetime.ApplicationStopping.Register(Stop);
 2168        }
 69
 70        /// <inheritdoc />
 71        public void ReportFileSystemChangeBeginning(string path)
 72        {
 073            ArgumentException.ThrowIfNullOrEmpty(path);
 74
 075            _tempIgnoredPaths[path] = path;
 076        }
 77
 78        /// <inheritdoc />
 79        public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
 80        {
 081            ArgumentException.ThrowIfNullOrEmpty(path);
 82
 83            // This is an arbitrary amount of time, but delay it because file system writes often trigger events long af
 84            // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
 85            // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, o
 086            await Task.Delay(45000).ConfigureAwait(false);
 87
 088            _tempIgnoredPaths.TryRemove(path, out _);
 89
 090            if (refreshPath)
 91            {
 92                try
 93                {
 094                    ReportFileSystemChanged(path);
 095                }
 096                catch (Exception ex)
 97                {
 098                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
 099                }
 100            }
 0101        }
 102
 103        private bool IsLibraryMonitorEnabled(BaseItem item)
 104        {
 42105            if (item is BasePluginFolder)
 106            {
 42107                return false;
 108            }
 109
 0110            var options = _libraryManager.GetLibraryOptions(item);
 111
 0112            return options is not null && options.EnableRealtimeMonitor;
 113        }
 114
 115        /// <inheritdoc />
 116        public void Start()
 117        {
 42118            _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
 42119            _libraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
 120
 42121            var pathsToWatch = new List<string>();
 122
 42123            var paths = _libraryManager
 42124                .RootFolder
 42125                .Children
 42126                .Where(IsLibraryMonitorEnabled)
 42127                .OfType<Folder>()
 42128                .SelectMany(f => f.PhysicalLocations)
 42129                .Distinct()
 42130                .Order();
 131
 84132            foreach (var path in paths)
 133            {
 0134                if (!ContainsParentFolder(pathsToWatch, path))
 135                {
 0136                    pathsToWatch.Add(path);
 137                }
 138            }
 139
 84140            foreach (var path in pathsToWatch)
 141            {
 0142                StartWatchingPath(path);
 143            }
 42144        }
 145
 146        private void StartWatching(BaseItem item)
 147        {
 0148            if (IsLibraryMonitorEnabled(item))
 149            {
 0150                StartWatchingPath(item.Path);
 151            }
 0152        }
 153
 154        /// <summary>
 155        /// Handles the ItemRemoved event of the LibraryManager control.
 156        /// </summary>
 157        /// <param name="sender">The source of the event.</param>
 158        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 159        private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
 160        {
 0161            if (e.Parent is AggregateFolder)
 162            {
 0163                StopWatchingPath(e.Item.Path);
 164            }
 0165        }
 166
 167        /// <summary>
 168        /// Handles the ItemAdded event of the LibraryManager control.
 169        /// </summary>
 170        /// <param name="sender">The source of the event.</param>
 171        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 172        private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
 173        {
 0174            if (e.Parent is AggregateFolder)
 175            {
 0176                StartWatching(e.Item);
 177            }
 0178        }
 179
 180        /// <summary>
 181        /// Examine a list of strings assumed to be file paths to see if it contains a parent of
 182        /// the provided path.
 183        /// </summary>
 184        /// <param name="lst">The LST.</param>
 185        /// <param name="path">The path.</param>
 186        /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
 187        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
 188        private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
 189        {
 0190            if (path.IsEmpty)
 191            {
 0192                throw new ArgumentException("Path can't be empty", nameof(path));
 193            }
 194
 0195            path = path.TrimEnd(Path.DirectorySeparatorChar);
 196
 0197            foreach (var str in lst)
 198            {
 199                // this should be a little quicker than examining each actual parent folder...
 0200                var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
 201
 0202                if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
 0203                    || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.Dir
 204                {
 0205                    return true;
 206                }
 207            }
 208
 0209            return false;
 0210        }
 211
 212        /// <summary>
 213        /// Starts the watching path.
 214        /// </summary>
 215        /// <param name="path">The path.</param>
 216        private void StartWatchingPath(string path)
 217        {
 0218            if (!Directory.Exists(path))
 219            {
 220                // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
 0221                _logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
 0222                return;
 223            }
 224
 225            // Already being watched
 0226            if (_fileSystemWatchers.ContainsKey(path))
 227            {
 0228                return;
 229            }
 230
 231            // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do t
 0232            Task.Run(() =>
 0233            {
 0234                try
 0235                {
 0236                    var newWatcher = new FileSystemWatcher(path, "*")
 0237                    {
 0238                        IncludeSubdirectories = true,
 0239                        InternalBufferSize = 65536,
 0240                        NotifyFilter = NotifyFilters.CreationTime |
 0241                                       NotifyFilters.DirectoryName |
 0242                                       NotifyFilters.FileName |
 0243                                       NotifyFilters.LastWrite |
 0244                                       NotifyFilters.Size |
 0245                                       NotifyFilters.Attributes
 0246                    };
 0247
 0248                    newWatcher.Created += OnWatcherChanged;
 0249                    newWatcher.Deleted += OnWatcherChanged;
 0250                    newWatcher.Renamed += OnWatcherChanged;
 0251                    newWatcher.Changed += OnWatcherChanged;
 0252                    newWatcher.Error += OnWatcherError;
 0253
 0254                    if (_fileSystemWatchers.TryAdd(path, newWatcher))
 0255                    {
 0256                        newWatcher.EnableRaisingEvents = true;
 0257                        _logger.LogInformation("Watching directory {Path}", path);
 0258                    }
 0259                    else
 0260                    {
 0261                        DisposeWatcher(newWatcher, false);
 0262                    }
 0263                }
 0264                catch (Exception ex)
 0265                {
 0266                    _logger.LogError(ex, "Error watching path: {Path}", path);
 0267                }
 0268            });
 0269        }
 270
 271        /// <summary>
 272        /// Stops the watching path.
 273        /// </summary>
 274        /// <param name="path">The path.</param>
 275        private void StopWatchingPath(string path)
 276        {
 0277            if (_fileSystemWatchers.TryGetValue(path, out var watcher))
 278            {
 0279                DisposeWatcher(watcher, true);
 280            }
 0281        }
 282
 283        /// <summary>
 284        /// Disposes the watcher.
 285        /// </summary>
 286        private void DisposeWatcher(FileSystemWatcher watcher, bool removeFromList)
 287        {
 288            try
 289            {
 0290                using (watcher)
 291                {
 0292                    _logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
 293
 0294                    watcher.Created -= OnWatcherChanged;
 0295                    watcher.Deleted -= OnWatcherChanged;
 0296                    watcher.Renamed -= OnWatcherChanged;
 0297                    watcher.Changed -= OnWatcherChanged;
 0298                    watcher.Error -= OnWatcherError;
 299
 0300                    watcher.EnableRaisingEvents = false;
 0301                }
 302            }
 303            finally
 304            {
 0305                if (removeFromList)
 306                {
 0307                    _fileSystemWatchers.TryRemove(watcher.Path, out _);
 308                }
 0309            }
 0310        }
 311
 312        /// <summary>
 313        /// Handles the Error event of the watcher control.
 314        /// </summary>
 315        /// <param name="sender">The source of the event.</param>
 316        /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
 317        private void OnWatcherError(object sender, ErrorEventArgs e)
 318        {
 0319            var ex = e.GetException();
 0320            var dw = (FileSystemWatcher)sender;
 321
 0322            if (ex is UnauthorizedAccessException unauthorizedAccessException)
 323            {
 0324                _logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path)
 0325                return;
 326            }
 327
 0328            _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
 329
 0330            DisposeWatcher(dw, true);
 0331        }
 332
 333        /// <summary>
 334        /// Handles the Changed event of the watcher control.
 335        /// </summary>
 336        /// <param name="sender">The source of the event.</param>
 337        /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
 338        private void OnWatcherChanged(object sender, FileSystemEventArgs e)
 339        {
 340            try
 341            {
 0342                ReportFileSystemChanged(e.FullPath);
 0343            }
 0344            catch (Exception ex)
 345            {
 0346                _logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
 0347            }
 0348        }
 349
 350        /// <inheritdoc />
 351        public void ReportFileSystemChanged(string path)
 352        {
 0353            ArgumentException.ThrowIfNullOrEmpty(path);
 354
 0355            if (IgnorePatterns.ShouldIgnore(path))
 356            {
 0357                return;
 358            }
 359
 0360            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 0361            if (_dotIgnoreIgnoreRule.ShouldIgnore(fileInfo, null))
 362            {
 0363                return;
 364            }
 365
 366            // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
 0367            foreach (var i in _tempIgnoredPaths.Keys)
 368            {
 0369                if (_fileSystem.AreEqual(i, path)
 0370                    || _fileSystem.ContainsSubPath(i, path))
 371                {
 0372                    _logger.LogDebug("Ignoring change to {Path}", path);
 0373                    return;
 374                }
 375
 376                // Go up a level
 0377                var parent = Path.GetDirectoryName(i);
 0378                if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
 379                {
 0380                    _logger.LogDebug("Ignoring change to {Path}", path);
 0381                    return;
 382                }
 383            }
 384
 0385            CreateRefresher(path);
 0386        }
 387
 388        private void CreateRefresher(string path)
 389        {
 0390            var parentPath = Path.GetDirectoryName(path);
 391
 0392            lock (_activeRefreshers)
 393            {
 0394                foreach (var refresher in _activeRefreshers)
 395                {
 396                    // Path is already being refreshed
 0397                    if (_fileSystem.AreEqual(path, refresher.Path))
 398                    {
 0399                        refresher.RestartTimer();
 0400                        return;
 401                    }
 402
 403                    // Parent folder is already being refreshed
 0404                    if (_fileSystem.ContainsSubPath(refresher.Path, path))
 405                    {
 0406                        refresher.AddPath(path);
 0407                        return;
 408                    }
 409
 410                    // New path is a parent
 0411                    if (_fileSystem.ContainsSubPath(path, refresher.Path))
 412                    {
 0413                        refresher.ResetPath(path, null);
 0414                        return;
 415                    }
 416
 417                    // They are siblings. Rebase the refresher to the parent folder.
 0418                    if (parentPath is not null
 0419                        && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
 420                    {
 0421                        refresher.ResetPath(parentPath, path);
 0422                        return;
 423                    }
 424                }
 425
 0426                var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
 0427                newRefresher.Completed += OnNewRefresherCompleted;
 0428                _activeRefreshers.Add(newRefresher);
 0429            }
 0430        }
 431
 432        private void OnNewRefresherCompleted(object? sender, EventArgs e)
 433        {
 0434            if (sender is null)
 435            {
 0436                return;
 437            }
 438
 0439            var refresher = (FileRefresher)sender;
 0440            DisposeRefresher(refresher);
 0441        }
 442
 443        /// <summary>
 444        /// Stops this instance.
 445        /// </summary>
 446        public void Stop()
 447        {
 66448            _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
 66449            _libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
 450
 132451            foreach (var watcher in _fileSystemWatchers.Values.ToList())
 452            {
 0453                DisposeWatcher(watcher, false);
 454            }
 455
 66456            _fileSystemWatchers.Clear();
 66457            DisposeRefreshers();
 66458        }
 459
 460        private void DisposeRefresher(FileRefresher refresher)
 461        {
 0462            lock (_activeRefreshers)
 463            {
 0464                refresher.Completed -= OnNewRefresherCompleted;
 0465                refresher.Dispose();
 0466                _activeRefreshers.Remove(refresher);
 0467            }
 0468        }
 469
 470        private void DisposeRefreshers()
 471        {
 66472            lock (_activeRefreshers)
 473            {
 132474                foreach (var refresher in _activeRefreshers)
 475                {
 0476                    refresher.Completed -= OnNewRefresherCompleted;
 0477                    refresher.Dispose();
 478                }
 479
 66480                _activeRefreshers.Clear();
 66481            }
 66482        }
 483
 484        /// <inheritdoc />
 485        public void Dispose()
 486        {
 21487            if (_disposed)
 488            {
 0489                return;
 490            }
 491
 21492            Stop();
 21493            _disposed = true;
 21494        }
 495    }
 496}