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 MoreLinq; using System; using System.Collections.Generic; using System.Linq; using System.Threading; namespace MediaBrowser.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; /// /// 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 Timer LibraryUpdateTimer { get; set; } /// /// The library update duration /// private const int LibraryUpdateDuration = 20000; public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger) { _libraryManager = libraryManager; _sessionManager = sessionManager; _userManager = userManager; _logger = logger; } public void Run() { _libraryManager.ItemAdded += libraryManager_ItemAdded; _libraryManager.ItemUpdated += libraryManager_ItemUpdated; _libraryManager.ItemRemoved += libraryManager_ItemRemoved; } /// /// 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 (e.Item.LocationType == LocationType.Virtual) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer == null) { LibraryUpdateTimer = new Timer(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 (e.Item.LocationType == LocationType.Virtual) { 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. void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) { if (e.Item.LocationType == LocationType.Virtual) { return; } lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer == null) { LibraryUpdateTimer = new Timer(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) { var info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, id); 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); var collections = user.RootFolder.GetChildren(user, true).ToList(); var allRecursiveChildren = user.RootFolder.GetRecursiveChildren(user) .Select(i => i.Id) .Distinct() .ToDictionary(i => i); return new LibraryUpdateInfo { ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, collections, allRecursiveChildren)).Select(i => i.Id).Distinct().ToList(), ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, collections, allRecursiveChildren)).Select(i => i.Id).Distinct().ToList(), ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, collections, allRecursiveChildren, true)).Select(i => i.Id).Distinct().ToList(), FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, collections, allRecursiveChildren)).Select(i => i.Id).Distinct().ToList(), FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, collections, allRecursiveChildren)).Select(i => i.Id).Distinct().ToList() }; } /// /// Translates the physical item to user library. /// /// /// The item. /// The user. /// The collections. /// All recursive children. /// if set to true [include if not found]. /// IEnumerable{``0}. private IEnumerable TranslatePhysicalItemToUserLibrary(T item, User user, IEnumerable collections, Dictionary allRecursiveChildren, 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 }; } // Need to find what user collection folder this belongs to if (item.Parent is AggregateFolder) { if (item.LocationType == LocationType.FileSystem) { return collections.Where(i => i.PhysicalLocations.Contains(item.Path)).Cast(); } } // If it's a user root, return it only if it's the right one if (item is UserRootFolder) { if (item.Id == user.RootFolder.Id) { return new[] { item }; } return new T[] { }; } // Return it only if it's in the user's library if (includeIfNotFound || allRecursiveChildren.ContainsKey(item.Id)) { 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; } } } }