using MediaBrowser.Common.Events; using MediaBrowser.Common.Kernel; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Connectivity; using MediaBrowser.Model.Logging; using System; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Controller.Library { /// /// Class UserDataManager /// public class UserDataManager : BaseManager { #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 /// /// The _logger /// private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The kernel. /// The logger. public UserDataManager(Kernel kernel, ILogger logger) : base(kernel) { _logger = logger; } /// /// Used to report that playback has started for an item /// /// The user. /// The item. /// Type of the client. /// Name of the device. /// public void OnPlaybackStart(User user, BaseItem item, ClientType clientType, string deviceName) { if (user == null) { throw new ArgumentNullException(); } if (item == null) { throw new ArgumentNullException(); } Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item); // Nothing to save here // Fire events to inform plugins EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs { Argument = item, User = user }, _logger); } /// /// Used to report playback progress for an item /// /// The user. /// The item. /// The position ticks. /// Type of the client. /// Name of the device. /// Task. /// public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName) { if (user == null) { throw new ArgumentNullException(); } if (item == null) { throw new ArgumentNullException(); } Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item, positionTicks); if (positionTicks.HasValue) { var data = item.GetUserData(user, true); UpdatePlayState(item, data, positionTicks.Value, false); await SaveUserDataForItem(user, item, data).ConfigureAwait(false); } EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs { Argument = 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. /// Name of the device. /// Task. /// public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName) { if (user == null) { throw new ArgumentNullException(); } if (item == null) { throw new ArgumentNullException(); } Kernel.UserManager.RemoveNowPlayingItemId(user, clientType, deviceName, item); var data = item.GetUserData(user, true); 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 SaveUserDataForItem(user, item, data).ConfigureAwait(false); EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs { Argument = 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 < Kernel.Configuration.MinResumePct) { positionTicks = 0; incrementPlayCount = false; } // If we're at the end, assume completed else if (pctIn > Kernel.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) { positionTicks = 0; data.Played = true; } else { // Enforce MinResumeDuration var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; if (durationSeconds < Kernel.Configuration.MinResumeDurationSeconds) { positionTicks = 0; data.Played = true; } } } data.PlaybackPositionTicks = positionTicks; if (incrementPlayCount) { data.PlayCount++; data.LastPlayedDate = DateTime.UtcNow; } } /// /// Saves user data for an item /// /// The user. /// The item. /// The data. public Task SaveUserDataForItem(User user, BaseItem item, UserItemData data) { item.AddOrUpdateUserData(user, data); return Kernel.UserDataRepository.SaveUserData(item, CancellationToken.None); } } }