using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Events; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Threading; namespace Emby.Server.Implementations.EntryPoints { public class LibraryChangedNotifier : IServerEntryPoint { /// /// The _library manager /// private readonly ILibraryManager _libraryManager; private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; private readonly ILogger _logger; private readonly ITimerFactory _timerFactory; /// /// 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(); /// /// Gets or sets the library update timer. /// /// The library update timer. private ITimer LibraryUpdateTimer { get; set; } /// /// The library update duration /// private const int LibraryUpdateDuration = 5000; private readonly IProviderManager _providerManager; public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger, ITimerFactory timerFactory, IProviderManager providerManager) { _libraryManager = libraryManager; _sessionManager = sessionManager; _userManager = userManager; _logger = logger; _timerFactory = timerFactory; _providerManager = providerManager; } public void Run() { _libraryManager.ItemAdded += libraryManager_ItemAdded; _libraryManager.ItemUpdated += libraryManager_ItemUpdated; _libraryManager.ItemRemoved += libraryManager_ItemRemoved; _providerManager.RefreshCompleted += _providerManager_RefreshCompleted; _providerManager.RefreshStarted += _providerManager_RefreshStarted; _providerManager.RefreshProgress += _providerManager_RefreshProgress; } private Dictionary _lastProgressMessageTimes = new Dictionary(); private void _providerManager_RefreshProgress(object sender, GenericEventArgs> e) { var item = e.Argument.Item1; if (!EnableRefreshMessage(item)) { return; } var progress = e.Argument.Item2; DateTime lastMessageSendTime; if (_lastProgressMessageTimes.TryGetValue(item.Id, out lastMessageSendTime)) { if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000) { return; } } _lastProgressMessageTimes[item.Id] = DateTime.UtcNow; var dict = new Dictionary(); dict["ItemId"] = item.Id.ToString("N"); dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture); try { _sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None); _logger.Info("Sending refresh progress {0} {1}", item.Id.ToString("N"), progress); } catch { } var collectionFolders = _libraryManager.GetCollectionFolders(item).ToList(); foreach (var collectionFolder in collectionFolders) { var collectionFolderDict = new Dictionary(); collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N"); collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture); try { _sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None); } catch { } } } private void _providerManager_RefreshStarted(object sender, GenericEventArgs e) { _providerManager_RefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 0))); } private void _providerManager_RefreshCompleted(object sender, GenericEventArgs e) { _providerManager_RefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 100))); } private bool EnableRefreshMessage(BaseItem item) { var folder = item as Folder; if (folder == null) { 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. void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer == null) { LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else { LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); } if (e.Item.Parent != null) { _foldersAddedTo.Add(e.Item.Parent); } _itemsAdded.Add(e.Item); } } /// /// Handles the ItemUpdated event of the libraryManager control. /// /// The source of the event. /// The instance containing the event data. void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer == null) { LibraryUpdateTimer = _timerFactory.Create(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. void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer == null) { LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else { LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); } if (e.Item.Parent != null) { _foldersRemovedFrom.Add(e.Item.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.DistinctBy(i => i.Id).ToList(); var foldersRemovedFrom = _foldersRemovedFrom.DistinctBy(i => i.Id).ToList(); var itemsUpdated = _itemsUpdated .Where(i => !_itemsAdded.Contains(i)) .DistinctBy(i => i.Id) .ToList(); SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None); 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 void SendChangeNotifications(List itemsAdded, List itemsUpdated, List itemsRemoved, List foldersAddedTo, List foldersRemovedFrom, CancellationToken cancellationToken) { foreach (var user in _userManager.Users.ToList()) { var id = user.Id; var userSessions = _sessionManager.Sessions .Where(u => u.UserId.HasValue && u.UserId.Value == id && u.SessionController != null && u.IsActive) .ToList(); if (userSessions.Count > 0) { LibraryUpdateInfo info; try { info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, id); } catch (Exception ex) { _logger.ErrorException("Error in GetLibraryUpdateInfo", ex); return; } foreach (var userSession in userSessions) { try { await userSession.SessionController.SendLibraryUpdateInfo(info, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error sending LibraryChanged message", ex); } } } } } /// /// 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(IEnumerable itemsAdded, IEnumerable itemsUpdated, IEnumerable itemsRemoved, IEnumerable foldersAddedTo, IEnumerable foldersRemovedFrom, Guid userId) { var user = _userManager.GetUserById(userId); return new LibraryUpdateInfo { ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToList(), ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToList(), ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)).Select(i => i.Id.ToString("N")).Distinct().ToList(), FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToList(), FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToList() }; } private bool FilterItem(BaseItem item) { if (!item.IsFolder && item.LocationType == LocationType.Virtual) { return false; } if (item is IItemByName && !(item is MusicArtist)) { return false; } return item.SourceType == SourceType.Library; } /// /// Translates the physical item to user library. /// /// /// 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[] { user.RootFolder as T }; } // Return it only if it's in the user's library if (includeIfNotFound || item.IsVisibleStandalone(user)) { return new[] { item }; } return new T[] { }; } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); } /// /// 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 -= libraryManager_ItemAdded; _libraryManager.ItemUpdated -= libraryManager_ItemUpdated; _libraryManager.ItemRemoved -= libraryManager_ItemRemoved; } } } }