#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.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 { /// /// The library update duration. /// private const int LibraryUpdateDuration = 30000; private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; private readonly ILogger _logger; /// /// The library changed sync lock. /// private readonly object _libraryChangedSyncLock = new object(); private readonly List _foldersAddedTo = new List(); private readonly List _foldersRemovedFrom = new List(); private readonly List _itemsAdded = new List(); private readonly List _itemsRemoved = new List(); private readonly List _itemsUpdated = new List(); private readonly ConcurrentDictionary _lastProgressMessageTimes = new ConcurrentDictionary(); public LibraryChangedNotifier( ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger, IProviderManager providerManager) { _libraryManager = libraryManager; _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).ToList(); 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) { if (item is not Folder folder) { return false; } if (folder.IsRoot) { return false; } if (folder is AggregateFolder || folder is UserRootFolder) { return false; } if (folder is UserView || folder is Channel) { return false; } if (!folder.IsTopParent) { return false; } return true; } /// /// 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 == null) { LibraryUpdateTimer = new Timer( LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else { LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); } 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 == null) { LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else { LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); } _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 == null) { LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else { LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); } if (e.Parent is Folder parent) { _foldersRemovedFrom.Add(parent); } _itemsRemoved.Add(e.Item); } } /// /// Libraries the update timer callback. /// /// The state. private void LibraryUpdateTimerCallback(object state) { lock (_libraryChangedSyncLock) { // Remove dupes in case some were saved multiple times var foldersAddedTo = _foldersAddedTo .GroupBy(x => x.Id) .Select(x => x.First()) .ToList(); var foldersRemovedFrom = _foldersRemovedFrom .GroupBy(x => x.Id) .Select(x => x.First()) .ToList(); var itemsUpdated = _itemsUpdated .Where(i => !_itemsAdded.Contains(i)) .GroupBy(x => x.Id) .Select(x => x.First()) .ToList(); SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult(); if (LibraryUpdateTimer != null) { LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; } _itemsAdded.Clear(); _itemsRemoved.Clear(); _itemsUpdated.Clear(); _foldersAddedTo.Clear(); _foldersRemovedFrom.Clear(); } } /// /// 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 != null) { LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; } _libraryManager.ItemAdded -= OnLibraryItemAdded; _libraryManager.ItemUpdated -= OnLibraryItemUpdated; _libraryManager.ItemRemoved -= OnLibraryItemRemoved; _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; _providerManager.RefreshStarted -= OnProviderRefreshStarted; _providerManager.RefreshProgress -= OnProviderRefreshProgress; } } } }