#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { public class LibraryChangedNotifier : IServerEntryPoint { private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _configurationManager; private readonly IProviderManager _providerManager; private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; private readonly ILogger _logger; private readonly object _libraryChangedSyncLock = new(); private readonly List _foldersAddedTo = new(); private readonly List _foldersRemovedFrom = new(); private readonly List _itemsAdded = new(); private readonly List _itemsRemoved = new(); private readonly List _itemsUpdated = new(); private readonly ConcurrentDictionary _lastProgressMessageTimes = new(); public LibraryChangedNotifier( ILibraryManager libraryManager, IServerConfigurationManager configurationManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger, IProviderManager providerManager) { _libraryManager = libraryManager; _configurationManager = configurationManager; _sessionManager = sessionManager; _userManager = userManager; _logger = logger; _providerManager = providerManager; } /// /// Gets or sets the library update timer. /// /// The library update timer. private Timer LibraryUpdateTimer { get; set; } public Task RunAsync() { _libraryManager.ItemAdded += OnLibraryItemAdded; _libraryManager.ItemUpdated += OnLibraryItemUpdated; _libraryManager.ItemRemoved += OnLibraryItemRemoved; _providerManager.RefreshCompleted += OnProviderRefreshCompleted; _providerManager.RefreshStarted += OnProviderRefreshStarted; _providerManager.RefreshProgress += OnProviderRefreshProgress; return Task.CompletedTask; } private void OnProviderRefreshProgress(object sender, GenericEventArgs> e) { var item = e.Argument.Item1; if (!EnableRefreshMessage(item)) { return; } var progress = e.Argument.Item2; if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime)) { if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000) { return; } } _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow); var dict = new Dictionary(); dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture); try { _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None); } catch { } var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) { var collectionFolderDict = new Dictionary { ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture) }; try { _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None); } catch { } } } private void OnProviderRefreshStarted(object sender, GenericEventArgs e) { OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 0))); } private void OnProviderRefreshCompleted(object sender, GenericEventArgs e) { OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 100))); _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _); } private static bool EnableRefreshMessage(BaseItem item) => item is Folder { IsRoot: false, IsTopParent: true } and not (AggregateFolder or UserRootFolder or UserView or Channel); /// /// Handles the ItemAdded event of the libraryManager control. /// /// The source of the event. /// The instance containing the event data. private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer is null) { LibraryUpdateTimer = new Timer( LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } else { LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } if (e.Item.GetParent() is Folder parent) { _foldersAddedTo.Add(parent); } _itemsAdded.Add(e.Item); } } /// /// Handles the ItemUpdated event of the libraryManager control. /// /// The source of the event. /// The instance containing the event data. private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer is null) { LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } else { LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } _itemsUpdated.Add(e.Item); } } /// /// Handles the ItemRemoved event of the libraryManager control. /// /// The source of the event. /// The instance containing the event data. private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer is null) { LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } else { LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } if (e.Parent is Folder parent) { _foldersRemovedFrom.Add(parent); } _itemsRemoved.Add(e.Item); } } /// /// Libraries the update timer callback. /// /// The state. private async void LibraryUpdateTimerCallback(object state) { List foldersAddedTo; List foldersRemovedFrom; List itemsUpdated; List itemsAdded; List itemsRemoved; lock (_libraryChangedSyncLock) { // Remove dupes in case some were saved multiple times foldersAddedTo = _foldersAddedTo .DistinctBy(x => x.Id) .ToList(); foldersRemovedFrom = _foldersRemovedFrom .DistinctBy(x => x.Id) .ToList(); itemsUpdated = _itemsUpdated .Where(i => !_itemsAdded.Contains(i)) .DistinctBy(x => x.Id) .ToList(); itemsAdded = _itemsAdded.ToList(); itemsRemoved = _itemsRemoved.ToList(); if (LibraryUpdateTimer is not null) { LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; } _itemsAdded.Clear(); _itemsRemoved.Clear(); _itemsUpdated.Clear(); _foldersAddedTo.Clear(); _foldersRemovedFrom.Clear(); } await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); } /// /// Sends the change notifications. /// /// The items added. /// The items updated. /// The items removed. /// The folders added to. /// The folders removed from. /// The cancellation token. private async Task SendChangeNotifications( List itemsAdded, List itemsUpdated, List itemsRemoved, List foldersAddedTo, List foldersRemovedFrom, CancellationToken cancellationToken) { var userIds = _sessionManager.Sessions .Select(i => i.UserId) .Where(i => !i.Equals(default)) .Distinct() .ToArray(); foreach (var userId in userIds) { LibraryUpdateInfo info; try { info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId); } catch (Exception ex) { _logger.LogError(ex, "Error in GetLibraryUpdateInfo"); return; } if (info.IsEmpty) { continue; } try { await _sessionManager.SendMessageToUserSessions( new List { userId }, SessionMessageType.LibraryChanged, info, cancellationToken) .ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Error sending LibraryChanged message"); } } } /// /// Gets the library update info. /// /// The items added. /// The items updated. /// The items removed. /// The folders added to. /// The folders removed from. /// The user id. /// LibraryUpdateInfo. private LibraryUpdateInfo GetLibraryUpdateInfo( List itemsAdded, List itemsUpdated, List itemsRemoved, List foldersAddedTo, List foldersRemovedFrom, Guid userId) { var user = _userManager.GetUserById(userId); var newAndRemoved = new List(); newAndRemoved.AddRange(foldersAddedTo); newAndRemoved.AddRange(foldersRemovedFrom); var allUserRootChildren = _libraryManager.GetUserRootFolder() .GetChildren(user, true) .OfType() .ToList(); return new LibraryUpdateInfo { ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .Distinct() .ToArray(), ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .Distinct() .ToArray(), ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)) .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .Distinct() .ToArray(), FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .Distinct() .ToArray(), FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .Distinct() .ToArray(), CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() }; } private static bool FilterItem(BaseItem item) { if (!item.IsFolder && !item.HasPathProtocol) { return false; } if (item is IItemByName && item is not MusicArtist) { return false; } return item.SourceType == SourceType.Library; } private IEnumerable GetTopParentIds(List items, List allUserRootChildren) { var list = new List(); foreach (var item in items) { // If the physical root changed, return the user root if (item is AggregateFolder) { continue; } foreach (var folder in allUserRootChildren) { list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture)); } } return list.Distinct(StringComparer.Ordinal); } /// /// Translates the physical item to user library. /// /// The type of item. /// The item. /// The user. /// if set to true [include if not found]. /// IEnumerable{``0}. private IEnumerable TranslatePhysicalItemToUserLibrary(T item, User user, bool includeIfNotFound = false) where T : BaseItem { // If the physical root changed, return the user root if (item is AggregateFolder) { return new[] { _libraryManager.GetUserRootFolder() as T }; } // Return it only if it's in the user's library if (includeIfNotFound || item.IsVisibleStandalone(user)) { return new[] { item }; } return Array.Empty(); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { if (LibraryUpdateTimer is not null) { LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; } _libraryManager.ItemAdded -= OnLibraryItemAdded; _libraryManager.ItemUpdated -= OnLibraryItemUpdated; _libraryManager.ItemRemoved -= OnLibraryItemRemoved; _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; _providerManager.RefreshStarted -= OnProviderRefreshStarted; _providerManager.RefreshProgress -= OnProviderRefreshProgress; } } } }