using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Connectivity; using MediaBrowser.Model.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Library { /// /// Class UserManager /// public class UserManager : IUserManager { /// /// The _active connections /// private readonly ConcurrentDictionary _activeConnections = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// /// The _users /// private IEnumerable _users; /// /// The _user lock /// private object _usersSyncLock = new object(); /// /// The _users initialized /// private bool _usersInitialized; /// /// Gets the users. /// /// The users. public IEnumerable Users { get { // Call ToList to exhaust the stream because we'll be iterating over this multiple times LazyInitializer.EnsureInitialized(ref _users, ref _usersInitialized, ref _usersSyncLock, LoadUsers); return _users; } internal set { _users = value; if (value == null) { _usersInitialized = false; } } } /// /// Gets all connections. /// /// All connections. public IEnumerable AllConnections { get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); } } /// /// Gets the active connections. /// /// The active connections. public IEnumerable RecentConnections { get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 5); } } /// /// The _logger /// private readonly ILogger _logger; private readonly IUserDataRepository _userDataRepository; /// /// Gets or sets the configuration manager. /// /// The configuration manager. private IServerConfigurationManager ConfigurationManager { get; set; } /// /// Gets the active user repository /// /// The user repository. public IUserRepository UserRepository { get; set; } /// /// Initializes a new instance of the class. /// /// The logger. /// The configuration manager. /// The user data repository. public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserDataRepository userDataRepository) { _logger = logger; ConfigurationManager = configurationManager; _userDataRepository = userDataRepository; } #region Events /// /// Occurs when [playback start]. /// public event EventHandler PlaybackStart; /// /// Occurs when [playback progress]. /// public event EventHandler PlaybackProgress; /// /// Occurs when [playback stopped]. /// public event EventHandler PlaybackStopped; #endregion #region UserUpdated Event /// /// Occurs when [user updated]. /// public event EventHandler> UserUpdated; /// /// Called when [user updated]. /// /// The user. private void OnUserUpdated(User user) { EventHelper.QueueEventIfNotNull(UserUpdated, this, new GenericEventArgs { Argument = user }, _logger); } #endregion #region UserDeleted Event /// /// Occurs when [user deleted]. /// public event EventHandler> UserDeleted; /// /// Called when [user deleted]. /// /// The user. private void OnUserDeleted(User user) { EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs { Argument = user }, _logger); } #endregion /// /// Gets a User by Id /// /// The id. /// User. /// public User GetUserById(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } return Users.FirstOrDefault(u => u.Id == id); } /// /// Authenticates a User and returns a result indicating whether or not it succeeded /// /// The user. /// The password. /// Task{System.Boolean}. /// user public async Task AuthenticateUser(User user, string password) { if (user == null) { throw new ArgumentNullException("user"); } var existingPasswordString = string.IsNullOrEmpty(user.Password) ? GetSha1String(string.Empty) : user.Password; var success = string.Equals(existingPasswordString, password.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); // Update LastActivityDate and LastLoginDate, then save if (success) { user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; await UpdateUser(user).ConfigureAwait(false); } _logger.Info("Authentication request for {0} {1}.", user.Name, (success ? "has succeeded" : "has been denied")); return success; } /// /// Gets the sha1 string. /// /// The STR. /// System.String. private static string GetSha1String(string str) { using (var provider = SHA1.Create()) { var hash = provider.ComputeHash(Encoding.UTF8.GetBytes(str)); return BitConverter.ToString(hash).Replace("-", string.Empty); } } /// /// Logs the user activity. /// /// The user. /// Type of the client. /// The device id. /// Name of the device. /// Task. /// user public Task LogUserActivity(User user, string clientType, string deviceId, string deviceName) { if (user == null) { throw new ArgumentNullException("user"); } var activityDate = DateTime.UtcNow; var lastActivityDate = user.LastActivityDate; user.LastActivityDate = activityDate; LogConnection(user.Id, clientType, deviceId, deviceName, activityDate); // Don't log in the db anymore frequently than 10 seconds if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10) { return Task.FromResult(true); } // Save this directly. No need to fire off all the events for this. return UserRepository.SaveUser(user, CancellationToken.None); } /// /// Updates the now playing item id. /// /// The user. /// Type of the client. /// The device id. /// Name of the device. /// The item. /// The current position ticks. private void UpdateNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item, long? currentPositionTicks = null) { var conn = GetConnection(user.Id, clientType, deviceId, deviceName); conn.NowPlayingPositionTicks = currentPositionTicks; conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item); conn.LastActivityDate = DateTime.UtcNow; } /// /// Removes the now playing item id. /// /// The user. /// Type of the client. /// The device id. /// Name of the device. /// The item. private void RemoveNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item) { var conn = GetConnection(user.Id, clientType, deviceId, deviceName); if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString())) { conn.NowPlayingItem = null; conn.NowPlayingPositionTicks = null; } } /// /// Logs the connection. /// /// The user id. /// Type of the client. /// The device id. /// Name of the device. /// The last activity date. private void LogConnection(Guid userId, string clientType, string deviceId, string deviceName, DateTime lastActivityDate) { GetConnection(userId, clientType, deviceId, deviceName).LastActivityDate = lastActivityDate; } /// /// Gets the connection. /// /// The user id. /// Type of the client. /// The device id. /// Name of the device. /// ClientConnectionInfo. private ClientConnectionInfo GetConnection(Guid userId, string clientType, string deviceId, string deviceName) { var key = clientType + deviceId; var connection = _activeConnections.GetOrAdd(key, keyName => new ClientConnectionInfo { UserId = userId.ToString(), Client = clientType, DeviceName = deviceName, DeviceId = deviceId }); connection.UserId = userId.ToString(); return connection; } /// /// Loads the users from the repository /// /// IEnumerable{User}. private IEnumerable LoadUsers() { var users = UserRepository.RetrieveAllUsers().ToList(); // There always has to be at least one user. if (users.Count == 0) { var name = Environment.UserName; var user = InstantiateNewUser(name); var task = UserRepository.SaveUser(user, CancellationToken.None); // Hate having to block threads Task.WaitAll(task); users.Add(user); } return users; } /// /// Refreshes metadata for each user /// /// The cancellation token. /// if set to true [force]. /// Task. public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false) { var tasks = Users.Select(user => user.RefreshMetadata(cancellationToken, forceRefresh: force)).ToList(); return Task.WhenAll(tasks); } /// /// Renames the user. /// /// The user. /// The new name. /// Task. /// user /// public async Task RenameUser(User user, string newName) { if (user == null) { throw new ArgumentNullException("user"); } if (string.IsNullOrEmpty(newName)) { throw new ArgumentNullException("newName"); } if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); } if (user.Name.Equals(newName, StringComparison.Ordinal)) { throw new ArgumentException("The new and old names must be different."); } await user.Rename(newName); OnUserUpdated(user); } /// /// Updates the user. /// /// The user. /// user /// public async Task UpdateUser(User user) { if (user == null) { throw new ArgumentNullException("user"); } if (user.Id == Guid.Empty || !Users.Any(u => u.Id.Equals(user.Id))) { throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); } user.DateModified = DateTime.UtcNow; await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); OnUserUpdated(user); } /// /// Creates the user. /// /// The name. /// User. /// name /// public async Task CreateUser(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); } var user = InstantiateNewUser(name); var list = Users.ToList(); list.Add(user); Users = list; await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); return user; } /// /// Deletes the user. /// /// The user. /// Task. /// user /// public async Task DeleteUser(User user) { if (user == null) { throw new ArgumentNullException("user"); } if (Users.FirstOrDefault(u => u.Id == user.Id) == null) { throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); } if (Users.Count() == 1) { throw new ArgumentException(string.Format("The user '{0}' be deleted because there must be at least one user in the system.", user.Name)); } await UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false); OnUserDeleted(user); // Force this to be lazy loaded again Users = null; } /// /// Resets the password by clearing it. /// /// Task. public Task ResetPassword(User user) { return ChangePassword(user, string.Empty); } /// /// Changes the password. /// /// The user. /// The new password. /// Task. public Task ChangePassword(User user, string newPassword) { if (user == null) { throw new ArgumentNullException("user"); } user.Password = string.IsNullOrEmpty(newPassword) ? string.Empty : GetSha1String(newPassword); return UpdateUser(user); } /// /// Instantiates the new user. /// /// The name. /// User. private User InstantiateNewUser(string name) { return new User { Name = name, Id = ("MBUser" + name).GetMD5(), DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow }; } /// /// Used to report that playback has started for an item /// /// The user. /// The item. /// Type of the client. /// The device id. /// Name of the device. /// public void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName) { if (user == null) { throw new ArgumentNullException(); } if (item == null) { throw new ArgumentNullException(); } UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item); // Nothing to save here // Fire events to inform plugins EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs { Item = item, User = user }, _logger); } /// /// Used to report playback progress for an item /// /// The user. /// The item. /// The position ticks. /// Type of the client. /// The device id. /// Name of the device. /// Task. /// public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName) { if (user == null) { throw new ArgumentNullException(); } if (item == null) { throw new ArgumentNullException(); } UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item, positionTicks); var key = item.GetUserDataKey(); if (positionTicks.HasValue) { var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); UpdatePlayState(item, data, positionTicks.Value, false); await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); } EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs { Item = item, User = user, PlaybackPositionTicks = positionTicks }, _logger); } /// /// Used to report that playback has ended for an item /// /// The user. /// The item. /// The position ticks. /// Type of the client. /// The device id. /// Name of the device. /// Task. /// public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName) { if (user == null) { throw new ArgumentNullException(); } if (item == null) { throw new ArgumentNullException(); } RemoveNowPlayingItemId(user, clientType, deviceId, deviceName, item); var key = item.GetUserDataKey(); var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); if (positionTicks.HasValue) { UpdatePlayState(item, data, positionTicks.Value, true); } else { // If the client isn't able to report this, then we'll just have to make an assumption data.PlayCount++; data.Played = true; } await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs { Item = item, User = user, PlaybackPositionTicks = positionTicks }, _logger); } /// /// Updates playstate position for an item but does not save /// /// The item /// User data for the item /// The current playback position /// Whether or not to increment playcount private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount) { // If a position has been reported, and if we know the duration if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0) { var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; // Don't track in very beginning if (pctIn < ConfigurationManager.Configuration.MinResumePct) { positionTicks = 0; incrementPlayCount = false; } // If we're at the end, assume completed else if (pctIn > ConfigurationManager.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) { positionTicks = 0; data.Played = true; } else { // Enforce MinResumeDuration var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; if (durationSeconds < ConfigurationManager.Configuration.MinResumeDurationSeconds) { positionTicks = 0; data.Played = true; } } } data.PlaybackPositionTicks = positionTicks; if (incrementPlayCount) { data.PlayCount++; data.LastPlayedDate = DateTime.UtcNow; } } } }