using MediaBrowser.Common.Net; 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 IServerManager _serverManager; 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 = 60000; public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IServerManager serverManager, IUserManager userManager, ILogger logger) { _libraryManager = libraryManager; _sessionManager = sessionManager; _serverManager = serverManager; _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) { 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) { 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) { 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(IEnumerable itemsAdded, IEnumerable itemsUpdated, IEnumerable itemsRemoved, IEnumerable foldersAddedTo, IEnumerable foldersRemovedFrom, CancellationToken cancellationToken) { var currentSessions = _sessionManager.Sessions.ToList(); var users = currentSessions.Select(i => i.User) .Where(i => i != null) .Select(i => i.Id) .Distinct() .ToList(); foreach (var userId in users) { var id = userId; var webSockets = currentSessions.Where(u => u.User != null && u.User.Id == id) .SelectMany(i => i.WebSockets) .ToList(); try { await _serverManager.SendWebSocketMessageAsync("LibraryChanged", () => GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, id), webSockets, 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).ToList(); var allRecursiveChildren = user.RootFolder.GetRecursiveChildren(user).ToDictionary(i => i.Id); 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)).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. /// IEnumerable{``0}. private IEnumerable TranslatePhysicalItemToUserLibrary(T item, User user, IEnumerable collections, Dictionary allRecursiveChildren) where T : BaseItem { // If the physical root changed, return the user root if (item is AggregateFolder) { return new T[] { 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 => { try { return i.LocationType == LocationType.FileSystem && i.ResolveArgs.PhysicalLocations.Contains(item.Path); } catch (Exception ex) { _logger.ErrorException("Error getting ResolveArgs for {0}", ex, i.Path); return false; } }).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 T[] { item }; } return new T[] { }; } // Return it only if it's in the user's library if (allRecursiveChildren.ContainsKey(item.Id)) { return new T[] { 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; } } } }