< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.EntryPoints.LibraryChangedNotifier
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
Line coverage
44%
Covered lines: 57
Uncovered lines: 71
Coverable lines: 128
Total lines: 402
Line coverage: 44.5%
Branch coverage
35%
Covered branches: 20
Total branches: 56
Branch coverage: 35.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
StartAsync(...)100%11100%
StopAsync(...)100%11100%
OnProviderRefreshProgress(...)8.33%113.141211.11%
OnProviderRefreshStarted(...)100%11100%
OnProviderRefreshCompleted(...)100%11100%
EnableRefreshMessage(...)58.33%1212100%
OnLibraryItemAdded(...)100%11100%
OnLibraryItemUpdated(...)100%11100%
OnLibraryItemRemoved(...)100%11100%
OnLibraryChange(...)87.5%8.04891.66%
GetLibraryUpdateInfo(...)100%210%
FilterItem(...)50%12.1860%
GetTopParentIds(...)0%4260%
TranslatePhysicalItemToUserLibrary(...)0%7280%
Dispose()50%22100%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Globalization;
 5using System.Linq;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Data.Entities;
 9using Jellyfin.Data.Events;
 10using Jellyfin.Extensions;
 11using MediaBrowser.Controller.Channels;
 12using MediaBrowser.Controller.Configuration;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Entities.Audio;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.Providers;
 17using MediaBrowser.Controller.Session;
 18using MediaBrowser.Model.Entities;
 19using MediaBrowser.Model.Session;
 20using Microsoft.Extensions.Hosting;
 21using Microsoft.Extensions.Logging;
 22
 23namespace Emby.Server.Implementations.EntryPoints;
 24
 25/// <summary>
 26/// A <see cref="IHostedService"/> responsible for notifying users when libraries are updated.
 27/// </summary>
 28public sealed class LibraryChangedNotifier : IHostedService, IDisposable
 29{
 30    private readonly ILibraryManager _libraryManager;
 31    private readonly IServerConfigurationManager _configurationManager;
 32    private readonly IProviderManager _providerManager;
 33    private readonly ISessionManager _sessionManager;
 34    private readonly IUserManager _userManager;
 35    private readonly ILogger<LibraryChangedNotifier> _logger;
 36
 2237    private readonly object _libraryChangedSyncLock = new();
 2238    private readonly List<Folder> _foldersAddedTo = new();
 2239    private readonly List<Folder> _foldersRemovedFrom = new();
 2240    private readonly List<BaseItem> _itemsAdded = new();
 2241    private readonly List<BaseItem> _itemsRemoved = new();
 2242    private readonly List<BaseItem> _itemsUpdated = new();
 2243    private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new();
 44
 45    private Timer? _libraryUpdateTimer;
 46
 47    /// <summary>
 48    /// Initializes a new instance of the <see cref="LibraryChangedNotifier"/> class.
 49    /// </summary>
 50    /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
 51    /// <param name="configurationManager">The <see cref="IServerConfigurationManager"/>.</param>
 52    /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
 53    /// <param name="userManager">The <see cref="IUserManager"/>.</param>
 54    /// <param name="logger">The <see cref="ILogger"/>.</param>
 55    /// <param name="providerManager">The <see cref="IProviderManager"/>.</param>
 56    public LibraryChangedNotifier(
 57        ILibraryManager libraryManager,
 58        IServerConfigurationManager configurationManager,
 59        ISessionManager sessionManager,
 60        IUserManager userManager,
 61        ILogger<LibraryChangedNotifier> logger,
 62        IProviderManager providerManager)
 63    {
 2264        _libraryManager = libraryManager;
 2265        _configurationManager = configurationManager;
 2266        _sessionManager = sessionManager;
 2267        _userManager = userManager;
 2268        _logger = logger;
 2269        _providerManager = providerManager;
 2270    }
 71
 72    /// <inheritdoc />
 73    public Task StartAsync(CancellationToken cancellationToken)
 74    {
 2275        _libraryManager.ItemAdded += OnLibraryItemAdded;
 2276        _libraryManager.ItemUpdated += OnLibraryItemUpdated;
 2277        _libraryManager.ItemRemoved += OnLibraryItemRemoved;
 78
 2279        _providerManager.RefreshCompleted += OnProviderRefreshCompleted;
 2280        _providerManager.RefreshStarted += OnProviderRefreshStarted;
 2281        _providerManager.RefreshProgress += OnProviderRefreshProgress;
 82
 2283        return Task.CompletedTask;
 84    }
 85
 86    /// <inheritdoc />
 87    public Task StopAsync(CancellationToken cancellationToken)
 88    {
 2289        _libraryManager.ItemAdded -= OnLibraryItemAdded;
 2290        _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
 2291        _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
 92
 2293        _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
 2294        _providerManager.RefreshStarted -= OnProviderRefreshStarted;
 2295        _providerManager.RefreshProgress -= OnProviderRefreshProgress;
 96
 2297        return Task.CompletedTask;
 98    }
 99
 100    private void OnProviderRefreshProgress(object? sender, GenericEventArgs<Tuple<BaseItem, double>> e)
 101    {
 95102        var item = e.Argument.Item1;
 103
 95104        if (!EnableRefreshMessage(item))
 105        {
 95106            return;
 107        }
 108
 0109        var progress = e.Argument.Item2;
 110
 0111        if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime))
 112        {
 0113            if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000)
 114            {
 0115                return;
 116            }
 117        }
 118
 0119        _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
 120
 0121        var dict = new Dictionary<string, string>();
 0122        dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
 0123        dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture);
 124
 125        try
 126        {
 0127            _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None)
 0128        }
 0129        catch
 130        {
 0131        }
 132
 0133        var collectionFolders = _libraryManager.GetCollectionFolders(item);
 134
 0135        foreach (var collectionFolder in collectionFolders)
 136        {
 0137            var collectionFolderDict = new Dictionary<string, string>
 0138            {
 0139                ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
 0140                ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
 0141            };
 142
 143            try
 144            {
 0145                _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, Can
 0146            }
 0147            catch
 148            {
 0149            }
 150        }
 0151    }
 152
 153    private void OnProviderRefreshStarted(object? sender, GenericEventArgs<BaseItem> e)
 19154        => OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e
 155
 156    private void OnProviderRefreshCompleted(object? sender, GenericEventArgs<BaseItem> e)
 157    {
 19158        OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Ar
 159
 19160        _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
 19161    }
 162
 163    private static bool EnableRefreshMessage(BaseItem item)
 95164        => item is Folder { IsRoot: false, IsTopParent: true }
 95165            and not (AggregateFolder or UserRootFolder or UserView or Channel);
 166
 167    private void OnLibraryItemAdded(object? sender, ItemChangeEventArgs e)
 1168        => OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo);
 169
 170    private void OnLibraryItemUpdated(object? sender, ItemChangeEventArgs e)
 58171        => OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null);
 172
 173    private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e)
 2174        => OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom);
 175
 176    private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder>? foldersList)
 177    {
 61178        if (!FilterItem(item))
 179        {
 0180            return;
 181        }
 182
 61183        lock (_libraryChangedSyncLock)
 184        {
 61185            var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
 186
 61187            if (_libraryUpdateTimer is null)
 188            {
 22189                _libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSp
 190            }
 191            else
 192            {
 39193                _libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan);
 194            }
 195
 61196            if (foldersList is not null && parent is Folder folder)
 197            {
 3198                foldersList.Add(folder);
 199            }
 200
 61201            itemsList.Add(item);
 61202        }
 61203    }
 204
 205    private async void LibraryUpdateTimerCallback(object? state)
 206    {
 207        List<Folder> foldersAddedTo;
 208        List<Folder> foldersRemovedFrom;
 209        List<BaseItem> itemsUpdated;
 210        List<BaseItem> itemsAdded;
 211        List<BaseItem> itemsRemoved;
 212        lock (_libraryChangedSyncLock)
 213        {
 214            // Remove dupes in case some were saved multiple times
 215            foldersAddedTo = _foldersAddedTo
 216                .DistinctBy(x => x.Id)
 217                .ToList();
 218
 219            foldersRemovedFrom = _foldersRemovedFrom
 220                .DistinctBy(x => x.Id)
 221                .ToList();
 222
 223            itemsUpdated = _itemsUpdated
 224                .Where(i => !_itemsAdded.Contains(i))
 225                .DistinctBy(x => x.Id)
 226                .ToList();
 227
 228            itemsAdded = _itemsAdded.ToList();
 229            itemsRemoved = _itemsRemoved.ToList();
 230
 231            if (_libraryUpdateTimer is not null)
 232            {
 233                _libraryUpdateTimer.Dispose();
 234                _libraryUpdateTimer = null;
 235            }
 236
 237            _itemsAdded.Clear();
 238            _itemsRemoved.Clear();
 239            _itemsUpdated.Clear();
 240            _foldersAddedTo.Clear();
 241            _foldersRemovedFrom.Clear();
 242        }
 243
 244        await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, Cancel
 245    }
 246
 247    private async Task SendChangeNotifications(
 248        List<BaseItem> itemsAdded,
 249        List<BaseItem> itemsUpdated,
 250        List<BaseItem> itemsRemoved,
 251        List<Folder> foldersAddedTo,
 252        List<Folder> foldersRemovedFrom,
 253        CancellationToken cancellationToken)
 254    {
 255        var userIds = _sessionManager.Sessions
 256            .Select(i => i.UserId)
 257            .Where(i => !i.IsEmpty())
 258            .Distinct()
 259            .ToArray();
 260
 261        foreach (var userId in userIds)
 262        {
 263            LibraryUpdateInfo info;
 264
 265            try
 266            {
 267                info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, 
 268            }
 269            catch (Exception ex)
 270            {
 271                _logger.LogError(ex, "Error in GetLibraryUpdateInfo");
 272                return;
 273            }
 274
 275            if (info.IsEmpty)
 276            {
 277                continue;
 278            }
 279
 280            try
 281            {
 282                await _sessionManager.SendMessageToUserSessions(
 283                        new List<Guid> { userId },
 284                        SessionMessageType.LibraryChanged,
 285                        info,
 286                        cancellationToken)
 287                    .ConfigureAwait(false);
 288            }
 289            catch (Exception ex)
 290            {
 291                _logger.LogError(ex, "Error sending LibraryChanged message");
 292            }
 293        }
 294    }
 295
 296    private LibraryUpdateInfo GetLibraryUpdateInfo(
 297        List<BaseItem> itemsAdded,
 298        List<BaseItem> itemsUpdated,
 299        List<BaseItem> itemsRemoved,
 300        List<Folder> foldersAddedTo,
 301        List<Folder> foldersRemovedFrom,
 302        Guid userId)
 303    {
 0304        var user = _userManager.GetUserById(userId);
 0305        ArgumentNullException.ThrowIfNull(user);
 306
 0307        var newAndRemoved = new List<BaseItem>();
 0308        newAndRemoved.AddRange(foldersAddedTo);
 0309        newAndRemoved.AddRange(foldersRemovedFrom);
 310
 0311        var allUserRootChildren = _libraryManager.GetUserRootFolder()
 0312            .GetChildren(user, true)
 0313            .OfType<Folder>()
 0314            .ToList();
 315
 0316        return new LibraryUpdateInfo
 0317        {
 0318            ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
 0319                .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
 0320                .Distinct()
 0321                .ToArray(),
 0322            ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
 0323                .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
 0324                .Distinct()
 0325                .ToArray(),
 0326            ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true))
 0327                .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
 0328                .Distinct()
 0329                .ToArray(),
 0330            FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
 0331                .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
 0332                .Distinct()
 0333                .ToArray(),
 0334            FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
 0335                .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
 0336                .Distinct()
 0337                .ToArray(),
 0338            CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
 0339        };
 340    }
 341
 342    private static bool FilterItem(BaseItem item)
 343    {
 61344        if (!item.IsFolder && !item.HasPathProtocol)
 345        {
 0346            return false;
 347        }
 348
 61349        if (item is IItemByName && item is not MusicArtist)
 350        {
 0351            return false;
 352        }
 353
 61354        return item.SourceType == SourceType.Library;
 355    }
 356
 357    private static IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
 358    {
 0359        var list = new List<string>();
 360
 0361        foreach (var item in items)
 362        {
 363            // If the physical root changed, return the user root
 0364            if (item is AggregateFolder)
 365            {
 366                continue;
 367            }
 368
 0369            foreach (var folder in allUserRootChildren)
 370            {
 0371                list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
 372            }
 373        }
 374
 0375        return list.Distinct(StringComparer.Ordinal);
 376    }
 377
 378    private T[] TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
 379        where T : BaseItem
 380    {
 381        // If the physical root changed, return the user root
 0382        if (item is AggregateFolder)
 383        {
 0384            return _libraryManager.GetUserRootFolder() is T t ? new[] { t } : Array.Empty<T>();
 385        }
 386
 387        // Return it only if it's in the user's library
 0388        if (includeIfNotFound || item.IsVisibleStandalone(user))
 389        {
 0390            return new[] { item };
 391        }
 392
 0393        return Array.Empty<T>();
 394    }
 395
 396    /// <inheritdoc />
 397    public void Dispose()
 398    {
 22399        _libraryUpdateTimer?.Dispose();
 22400        _libraryUpdateTimer = null;
 22401    }
 402}

Methods/Properties

.ctor(MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Session.ISessionManager,MediaBrowser.Controller.Library.IUserManager,Microsoft.Extensions.Logging.ILogger`1<Emby.Server.Implementations.EntryPoints.LibraryChangedNotifier>,MediaBrowser.Controller.Providers.IProviderManager)
StartAsync(System.Threading.CancellationToken)
StopAsync(System.Threading.CancellationToken)
OnProviderRefreshProgress(System.Object,Jellyfin.Data.Events.GenericEventArgs`1<System.Tuple`2<MediaBrowser.Controller.Entities.BaseItem,System.Double>>)
OnProviderRefreshStarted(System.Object,Jellyfin.Data.Events.GenericEventArgs`1<MediaBrowser.Controller.Entities.BaseItem>)
OnProviderRefreshCompleted(System.Object,Jellyfin.Data.Events.GenericEventArgs`1<MediaBrowser.Controller.Entities.BaseItem>)
EnableRefreshMessage(MediaBrowser.Controller.Entities.BaseItem)
OnLibraryItemAdded(System.Object,MediaBrowser.Controller.Library.ItemChangeEventArgs)
OnLibraryItemUpdated(System.Object,MediaBrowser.Controller.Library.ItemChangeEventArgs)
OnLibraryItemRemoved(System.Object,MediaBrowser.Controller.Library.ItemChangeEventArgs)
OnLibraryChange(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>)
GetLibraryUpdateInfo(System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>,System.Guid)
FilterItem(MediaBrowser.Controller.Entities.BaseItem)
GetTopParentIds(System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>)
TranslatePhysicalItemToUserLibrary(T,Jellyfin.Data.Entities.User,System.Boolean)
Dispose()