1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Hosting;
namespace Emby.Server.Implementations.EntryPoints
{
/// <summary>
/// <see cref="IHostedService"/> responsible for notifying users when associated item data is updated.
/// </summary>
public sealed class UserDataChangeNotifier : IHostedService, IDisposable
{
private const int UpdateDuration = 500;
internal const int MaxBatchSize = 2000;
private readonly ISessionManager _sessionManager;
private readonly IUserDataManager _userDataManager;
private readonly IUserManager _userManager;
private readonly Dictionary<Guid, Dictionary<Guid, BaseItem>> _changedItems = [];
private readonly Lock _syncLock = new();
private Timer? _updateTimer;
private int _changedItemCount;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
/// </summary>
/// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
public UserDataChangeNotifier(
IUserDataManager userDataManager,
ISessionManager sessionManager,
IUserManager userManager)
{
_userDataManager = userDataManager;
_sessionManager = sessionManager;
_userManager = userManager;
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
_userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
{
_userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
return Task.CompletedTask;
}
private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
{
return;
}
lock (_syncLock)
{
// The window runs from the first change of a batch and is never extended, so a stream
// of changes that never pauses - a library scan - still closes its batches instead of
// holding every item it touched alive until the stream stops.
_updateTimer ??= new Timer(
UpdateTimerCallback,
null,
UpdateDuration,
Timeout.Infinite);
if (!_changedItems.TryGetValue(e.UserId, out Dictionary<Guid, BaseItem>? keys))
{
keys = [];
_changedItems[e.UserId] = keys;
}
var baseItem = e.Item;
// Go up one level for indicators
if (baseItem is not null)
{
Track(keys, baseItem);
var parent = baseItem.GetOwner() ?? baseItem.GetParent();
if (parent is not null)
{
Track(keys, parent);
}
}
// A window long enough to cover a burst still has to give way once the batch is
// large enough to be worth sending on its own.
if (_changedItemCount >= MaxBatchSize)
{
_updateTimer.Change(0, Timeout.Infinite);
}
}
}
private void Track(Dictionary<Guid, BaseItem> keys, BaseItem item)
{
var before = keys.Count;
keys[item.Id] = item;
if (keys.Count != before)
{
_changedItemCount++;
}
}
private async void UpdateTimerCallback(object? state)
{
List<KeyValuePair<Guid, Dictionary<Guid, BaseItem>>> changes;
lock (_syncLock)
{
changes = _changedItems.ToList();
_changedItems.Clear();
_changedItemCount = 0;
if (_updateTimer is not null)
{
_updateTimer.Dispose();
_updateTimer = null;
}
}
if (changes.Count == 0)
{
return;
}
foreach (var (userId, changedItems) in changes)
{
await _sessionManager.SendMessageToUserSessions(
[userId],
SessionMessageType.UserDataChanged,
() => GetUserDataChangeInfo(userId, changedItems.Values),
default).ConfigureAwait(false);
}
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, IEnumerable<BaseItem> changedItems)
{
var user = _userManager.GetUserById(userId)
?? throw new ArgumentException("Invalid user ID", nameof(userId));
return new UserDataChangeInfo
{
UserId = userId,
UserDataList = changedItems
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
if (dto is null)
{
return null!;
}
dto.ItemId = i.Id;
return dto;
})
.Where(e => e is not null)
.ToArray()
};
}
/// <inheritdoc />
public void Dispose()
{
_updateTimer?.Dispose();
_updateTimer = null;
}
}
}
|