< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.EntryPoints.UserDataChangeNotifier
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
Line coverage
68%
Covered lines: 51
Uncovered lines: 24
Coverable lines: 75
Total lines: 187
Line coverage: 68%
Branch coverage
69%
Covered branches: 18
Total branches: 26
Branch coverage: 69.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/12/2026 - 12:15:57 AM Line coverage: 19.1% (13/68) Branch coverage: 5% (1/20) Total lines: 16710/29/2026 - 12:17:11 AM Line coverage: 68% (51/75) Branch coverage: 69.2% (18/26) Total lines: 187 5/12/2026 - 12:15:57 AM Line coverage: 19.1% (13/68) Branch coverage: 5% (1/20) Total lines: 16710/29/2026 - 12:17:11 AM Line coverage: 68% (51/75) Branch coverage: 69.2% (18/26) Total lines: 187

Coverage delta

Coverage delta 65 -65

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
StartAsync(...)100%11100%
StopAsync(...)100%11100%
OnUserDataManagerUserDataSaved(...)78.57%151485%
Track(...)50%22100%
UpdateTimerCallback()83.33%6694.11%
GetUserDataChangeInfo(...)0%620%
Dispose()50%22100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using MediaBrowser.Controller.Entities;
 7using MediaBrowser.Controller.Library;
 8using MediaBrowser.Controller.Session;
 9using MediaBrowser.Model.Entities;
 10using MediaBrowser.Model.Session;
 11using Microsoft.Extensions.Hosting;
 12
 13namespace Emby.Server.Implementations.EntryPoints
 14{
 15    /// <summary>
 16    /// <see cref="IHostedService"/> responsible for notifying users when associated item data is updated.
 17    /// </summary>
 18    public sealed class UserDataChangeNotifier : IHostedService, IDisposable
 19    {
 20        private const int UpdateDuration = 500;
 21        internal const int MaxBatchSize = 2000;
 22
 23        private readonly ISessionManager _sessionManager;
 24        private readonly IUserDataManager _userDataManager;
 25        private readonly IUserManager _userManager;
 26
 2827        private readonly Dictionary<Guid, Dictionary<Guid, BaseItem>> _changedItems = [];
 2828        private readonly Lock _syncLock = new();
 29
 30        private Timer? _updateTimer;
 31        private int _changedItemCount;
 32
 33        /// <summary>
 34        /// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
 35        /// </summary>
 36        /// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
 37        /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
 38        /// <param name="userManager">The <see cref="IUserManager"/>.</param>
 39        public UserDataChangeNotifier(
 40            IUserDataManager userDataManager,
 41            ISessionManager sessionManager,
 42            IUserManager userManager)
 43        {
 2844            _userDataManager = userDataManager;
 2845            _sessionManager = sessionManager;
 2846            _userManager = userManager;
 2847        }
 48
 49        /// <inheritdoc />
 50        public Task StartAsync(CancellationToken cancellationToken)
 51        {
 2852            _userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
 53
 2854            return Task.CompletedTask;
 55        }
 56
 57        /// <inheritdoc />
 58        public Task StopAsync(CancellationToken cancellationToken)
 59        {
 2860            _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
 61
 2862            return Task.CompletedTask;
 63        }
 64
 65        private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
 66        {
 1867            if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
 68            {
 069                return;
 70            }
 71
 72            lock (_syncLock)
 73            {
 74                // The window runs from the first change of a batch and is never extended, so a stream
 75                // of changes that never pauses - a library scan - still closes its batches instead of
 76                // holding every item it touched alive until the stream stops.
 1877                _updateTimer ??= new Timer(
 1878                    UpdateTimerCallback,
 1879                    null,
 1880                    UpdateDuration,
 1881                    Timeout.Infinite);
 82
 1883                if (!_changedItems.TryGetValue(e.UserId, out Dictionary<Guid, BaseItem>? keys))
 84                {
 185                    keys = [];
 186                    _changedItems[e.UserId] = keys;
 87                }
 88
 1889                var baseItem = e.Item;
 90
 91                // Go up one level for indicators
 1892                if (baseItem is not null)
 93                {
 1894                    Track(keys, baseItem);
 95
 1896                    var parent = baseItem.GetOwner() ?? baseItem.GetParent();
 97
 1898                    if (parent is not null)
 99                    {
 0100                        Track(keys, parent);
 101                    }
 102                }
 103
 104                // A window long enough to cover a burst still has to give way once the batch is
 105                // large enough to be worth sending on its own.
 18106                if (_changedItemCount >= MaxBatchSize)
 107                {
 0108                    _updateTimer.Change(0, Timeout.Infinite);
 109                }
 18110            }
 18111        }
 112
 113        private void Track(Dictionary<Guid, BaseItem> keys, BaseItem item)
 114        {
 18115            var before = keys.Count;
 18116            keys[item.Id] = item;
 117
 18118            if (keys.Count != before)
 119            {
 18120                _changedItemCount++;
 121            }
 18122        }
 123
 124        private async void UpdateTimerCallback(object? state)
 1125        {
 126            List<KeyValuePair<Guid, Dictionary<Guid, BaseItem>>> changes;
 127            lock (_syncLock)
 128            {
 1129                changes = _changedItems.ToList();
 1130                _changedItems.Clear();
 1131                _changedItemCount = 0;
 132
 1133                if (_updateTimer is not null)
 134                {
 1135                    _updateTimer.Dispose();
 1136                    _updateTimer = null;
 137                }
 1138            }
 139
 1140            if (changes.Count == 0)
 141            {
 0142                return;
 143            }
 144
 4145            foreach (var (userId, changedItems) in changes)
 146            {
 1147                await _sessionManager.SendMessageToUserSessions(
 1148                    [userId],
 1149                    SessionMessageType.UserDataChanged,
 1150                    () => GetUserDataChangeInfo(userId, changedItems.Values),
 1151                    default).ConfigureAwait(false);
 152            }
 1153        }
 154
 155        private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, IEnumerable<BaseItem> changedItems)
 156        {
 0157            var user = _userManager.GetUserById(userId)
 0158                ?? throw new ArgumentException("Invalid user ID", nameof(userId));
 159
 0160            return new UserDataChangeInfo
 0161            {
 0162                UserId = userId,
 0163                UserDataList = changedItems
 0164                    .Select(i =>
 0165                    {
 0166                        var dto = _userDataManager.GetUserDataDto(i, user);
 0167                        if (dto is null)
 0168                        {
 0169                            return null!;
 0170                        }
 0171
 0172                        dto.ItemId = i.Id;
 0173                        return dto;
 0174                    })
 0175                    .Where(e => e is not null)
 0176                    .ToArray()
 0177            };
 178        }
 179
 180        /// <inheritdoc />
 181        public void Dispose()
 182        {
 28183            _updateTimer?.Dispose();
 28184            _updateTimer = null;
 28185        }
 186    }
 187}