UserManager.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. using System.IO;
  2. using MediaBrowser.Common.Events;
  3. using MediaBrowser.Common.Extensions;
  4. using MediaBrowser.Controller.Configuration;
  5. using MediaBrowser.Controller.Dto;
  6. using MediaBrowser.Controller.Entities;
  7. using MediaBrowser.Controller.Library;
  8. using MediaBrowser.Controller.Persistence;
  9. using MediaBrowser.Model.Connectivity;
  10. using MediaBrowser.Model.Logging;
  11. using System;
  12. using System.Collections.Concurrent;
  13. using System.Collections.Generic;
  14. using System.Linq;
  15. using System.Security.Cryptography;
  16. using System.Text;
  17. using System.Threading;
  18. using System.Threading.Tasks;
  19. namespace MediaBrowser.Server.Implementations.Library
  20. {
  21. /// <summary>
  22. /// Class UserManager
  23. /// </summary>
  24. public class UserManager : IUserManager
  25. {
  26. /// <summary>
  27. /// The _active connections
  28. /// </summary>
  29. private readonly ConcurrentDictionary<string, ClientConnectionInfo> _activeConnections =
  30. new ConcurrentDictionary<string, ClientConnectionInfo>(StringComparer.OrdinalIgnoreCase);
  31. /// <summary>
  32. /// The _users
  33. /// </summary>
  34. private IEnumerable<User> _users;
  35. /// <summary>
  36. /// The _user lock
  37. /// </summary>
  38. private object _usersSyncLock = new object();
  39. /// <summary>
  40. /// The _users initialized
  41. /// </summary>
  42. private bool _usersInitialized;
  43. /// <summary>
  44. /// Gets the users.
  45. /// </summary>
  46. /// <value>The users.</value>
  47. public IEnumerable<User> Users
  48. {
  49. get
  50. {
  51. // Call ToList to exhaust the stream because we'll be iterating over this multiple times
  52. LazyInitializer.EnsureInitialized(ref _users, ref _usersInitialized, ref _usersSyncLock, LoadUsers);
  53. return _users;
  54. }
  55. internal set
  56. {
  57. _users = value;
  58. if (value == null)
  59. {
  60. _usersInitialized = false;
  61. }
  62. }
  63. }
  64. /// <summary>
  65. /// Gets all connections.
  66. /// </summary>
  67. /// <value>All connections.</value>
  68. public IEnumerable<ClientConnectionInfo> AllConnections
  69. {
  70. get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); }
  71. }
  72. /// <summary>
  73. /// Gets the active connections.
  74. /// </summary>
  75. /// <value>The active connections.</value>
  76. public IEnumerable<ClientConnectionInfo> RecentConnections
  77. {
  78. get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 5); }
  79. }
  80. /// <summary>
  81. /// The _logger
  82. /// </summary>
  83. private readonly ILogger _logger;
  84. private readonly IUserDataRepository _userDataRepository;
  85. /// <summary>
  86. /// Gets or sets the configuration manager.
  87. /// </summary>
  88. /// <value>The configuration manager.</value>
  89. private IServerConfigurationManager ConfigurationManager { get; set; }
  90. /// <summary>
  91. /// Gets the active user repository
  92. /// </summary>
  93. /// <value>The user repository.</value>
  94. public IUserRepository UserRepository { get; set; }
  95. /// <summary>
  96. /// Initializes a new instance of the <see cref="UserManager" /> class.
  97. /// </summary>
  98. /// <param name="logger">The logger.</param>
  99. /// <param name="configurationManager">The configuration manager.</param>
  100. /// <param name="userDataRepository">The user data repository.</param>
  101. public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserDataRepository userDataRepository)
  102. {
  103. _logger = logger;
  104. ConfigurationManager = configurationManager;
  105. _userDataRepository = userDataRepository;
  106. }
  107. #region Events
  108. /// <summary>
  109. /// Occurs when [playback start].
  110. /// </summary>
  111. public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
  112. /// <summary>
  113. /// Occurs when [playback progress].
  114. /// </summary>
  115. public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
  116. /// <summary>
  117. /// Occurs when [playback stopped].
  118. /// </summary>
  119. public event EventHandler<PlaybackProgressEventArgs> PlaybackStopped;
  120. #endregion
  121. #region UserUpdated Event
  122. /// <summary>
  123. /// Occurs when [user updated].
  124. /// </summary>
  125. public event EventHandler<GenericEventArgs<User>> UserUpdated;
  126. /// <summary>
  127. /// Called when [user updated].
  128. /// </summary>
  129. /// <param name="user">The user.</param>
  130. private void OnUserUpdated(User user)
  131. {
  132. EventHelper.QueueEventIfNotNull(UserUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger);
  133. }
  134. #endregion
  135. #region UserDeleted Event
  136. /// <summary>
  137. /// Occurs when [user deleted].
  138. /// </summary>
  139. public event EventHandler<GenericEventArgs<User>> UserDeleted;
  140. /// <summary>
  141. /// Called when [user deleted].
  142. /// </summary>
  143. /// <param name="user">The user.</param>
  144. private void OnUserDeleted(User user)
  145. {
  146. EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs<User> { Argument = user }, _logger);
  147. }
  148. #endregion
  149. /// <summary>
  150. /// Gets a User by Id
  151. /// </summary>
  152. /// <param name="id">The id.</param>
  153. /// <returns>User.</returns>
  154. /// <exception cref="System.ArgumentNullException"></exception>
  155. public User GetUserById(Guid id)
  156. {
  157. if (id == Guid.Empty)
  158. {
  159. throw new ArgumentNullException("id");
  160. }
  161. return Users.FirstOrDefault(u => u.Id == id);
  162. }
  163. /// <summary>
  164. /// Authenticates a User and returns a result indicating whether or not it succeeded
  165. /// </summary>
  166. /// <param name="user">The user.</param>
  167. /// <param name="password">The password.</param>
  168. /// <returns>Task{System.Boolean}.</returns>
  169. /// <exception cref="System.ArgumentNullException">user</exception>
  170. public async Task<bool> AuthenticateUser(User user, string password)
  171. {
  172. if (user == null)
  173. {
  174. throw new ArgumentNullException("user");
  175. }
  176. var existingPasswordString = string.IsNullOrEmpty(user.Password) ? GetSha1String(string.Empty) : user.Password;
  177. var success = string.Equals(existingPasswordString, password.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
  178. // Update LastActivityDate and LastLoginDate, then save
  179. if (success)
  180. {
  181. user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
  182. await UpdateUser(user).ConfigureAwait(false);
  183. }
  184. _logger.Info("Authentication request for {0} {1}.", user.Name, (success ? "has succeeded" : "has been denied"));
  185. return success;
  186. }
  187. /// <summary>
  188. /// Gets the sha1 string.
  189. /// </summary>
  190. /// <param name="str">The STR.</param>
  191. /// <returns>System.String.</returns>
  192. private static string GetSha1String(string str)
  193. {
  194. using (var provider = SHA1.Create())
  195. {
  196. var hash = provider.ComputeHash(Encoding.UTF8.GetBytes(str));
  197. return BitConverter.ToString(hash).Replace("-", string.Empty);
  198. }
  199. }
  200. /// <summary>
  201. /// Logs the user activity.
  202. /// </summary>
  203. /// <param name="user">The user.</param>
  204. /// <param name="clientType">Type of the client.</param>
  205. /// <param name="deviceId">The device id.</param>
  206. /// <param name="deviceName">Name of the device.</param>
  207. /// <returns>Task.</returns>
  208. /// <exception cref="System.ArgumentNullException">user</exception>
  209. public Task LogUserActivity(User user, string clientType, string deviceId, string deviceName)
  210. {
  211. if (user == null)
  212. {
  213. throw new ArgumentNullException("user");
  214. }
  215. var activityDate = DateTime.UtcNow;
  216. var lastActivityDate = user.LastActivityDate;
  217. user.LastActivityDate = activityDate;
  218. LogConnection(user.Id, clientType, deviceId, deviceName, activityDate);
  219. // Don't log in the db anymore frequently than 10 seconds
  220. if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10)
  221. {
  222. return Task.FromResult(true);
  223. }
  224. // Save this directly. No need to fire off all the events for this.
  225. return UserRepository.SaveUser(user, CancellationToken.None);
  226. }
  227. /// <summary>
  228. /// Updates the now playing item id.
  229. /// </summary>
  230. /// <param name="user">The user.</param>
  231. /// <param name="clientType">Type of the client.</param>
  232. /// <param name="deviceId">The device id.</param>
  233. /// <param name="deviceName">Name of the device.</param>
  234. /// <param name="item">The item.</param>
  235. /// <param name="currentPositionTicks">The current position ticks.</param>
  236. private void UpdateNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item, long? currentPositionTicks = null)
  237. {
  238. var conn = GetConnection(user.Id, clientType, deviceId, deviceName);
  239. conn.NowPlayingPositionTicks = currentPositionTicks;
  240. conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item);
  241. conn.LastActivityDate = DateTime.UtcNow;
  242. }
  243. /// <summary>
  244. /// Removes the now playing item id.
  245. /// </summary>
  246. /// <param name="user">The user.</param>
  247. /// <param name="clientType">Type of the client.</param>
  248. /// <param name="deviceId">The device id.</param>
  249. /// <param name="deviceName">Name of the device.</param>
  250. /// <param name="item">The item.</param>
  251. private void RemoveNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item)
  252. {
  253. var conn = GetConnection(user.Id, clientType, deviceId, deviceName);
  254. if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString()))
  255. {
  256. conn.NowPlayingItem = null;
  257. conn.NowPlayingPositionTicks = null;
  258. }
  259. }
  260. /// <summary>
  261. /// Logs the connection.
  262. /// </summary>
  263. /// <param name="userId">The user id.</param>
  264. /// <param name="clientType">Type of the client.</param>
  265. /// <param name="deviceId">The device id.</param>
  266. /// <param name="deviceName">Name of the device.</param>
  267. /// <param name="lastActivityDate">The last activity date.</param>
  268. private void LogConnection(Guid userId, string clientType, string deviceId, string deviceName, DateTime lastActivityDate)
  269. {
  270. GetConnection(userId, clientType, deviceId, deviceName).LastActivityDate = lastActivityDate;
  271. }
  272. /// <summary>
  273. /// Gets the connection.
  274. /// </summary>
  275. /// <param name="userId">The user id.</param>
  276. /// <param name="clientType">Type of the client.</param>
  277. /// <param name="deviceId">The device id.</param>
  278. /// <param name="deviceName">Name of the device.</param>
  279. /// <returns>ClientConnectionInfo.</returns>
  280. private ClientConnectionInfo GetConnection(Guid userId, string clientType, string deviceId, string deviceName)
  281. {
  282. var key = clientType + deviceId;
  283. var connection = _activeConnections.GetOrAdd(key, keyName => new ClientConnectionInfo
  284. {
  285. UserId = userId.ToString(),
  286. Client = clientType,
  287. DeviceName = deviceName,
  288. DeviceId = deviceId
  289. });
  290. connection.DeviceName = deviceName;
  291. connection.UserId = userId.ToString();
  292. return connection;
  293. }
  294. /// <summary>
  295. /// Loads the users from the repository
  296. /// </summary>
  297. /// <returns>IEnumerable{User}.</returns>
  298. private IEnumerable<User> LoadUsers()
  299. {
  300. var users = UserRepository.RetrieveAllUsers().ToList();
  301. // There always has to be at least one user.
  302. if (users.Count == 0)
  303. {
  304. var name = Environment.UserName;
  305. var user = InstantiateNewUser(name);
  306. var task = UserRepository.SaveUser(user, CancellationToken.None);
  307. // Hate having to block threads
  308. Task.WaitAll(task);
  309. users.Add(user);
  310. }
  311. return users;
  312. }
  313. /// <summary>
  314. /// Refreshes metadata for each user
  315. /// </summary>
  316. /// <param name="cancellationToken">The cancellation token.</param>
  317. /// <param name="force">if set to <c>true</c> [force].</param>
  318. /// <returns>Task.</returns>
  319. public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false)
  320. {
  321. var tasks = Users.Select(user => user.RefreshMetadata(cancellationToken, forceRefresh: force)).ToList();
  322. return Task.WhenAll(tasks);
  323. }
  324. /// <summary>
  325. /// Renames the user.
  326. /// </summary>
  327. /// <param name="user">The user.</param>
  328. /// <param name="newName">The new name.</param>
  329. /// <returns>Task.</returns>
  330. /// <exception cref="System.ArgumentNullException">user</exception>
  331. /// <exception cref="System.ArgumentException"></exception>
  332. public async Task RenameUser(User user, string newName)
  333. {
  334. if (user == null)
  335. {
  336. throw new ArgumentNullException("user");
  337. }
  338. if (string.IsNullOrEmpty(newName))
  339. {
  340. throw new ArgumentNullException("newName");
  341. }
  342. if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
  343. {
  344. throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName));
  345. }
  346. if (user.Name.Equals(newName, StringComparison.Ordinal))
  347. {
  348. throw new ArgumentException("The new and old names must be different.");
  349. }
  350. await user.Rename(newName);
  351. OnUserUpdated(user);
  352. }
  353. /// <summary>
  354. /// Updates the user.
  355. /// </summary>
  356. /// <param name="user">The user.</param>
  357. /// <exception cref="System.ArgumentNullException">user</exception>
  358. /// <exception cref="System.ArgumentException"></exception>
  359. public async Task UpdateUser(User user)
  360. {
  361. if (user == null)
  362. {
  363. throw new ArgumentNullException("user");
  364. }
  365. if (user.Id == Guid.Empty || !Users.Any(u => u.Id.Equals(user.Id)))
  366. {
  367. throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id));
  368. }
  369. user.DateModified = DateTime.UtcNow;
  370. await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
  371. OnUserUpdated(user);
  372. }
  373. /// <summary>
  374. /// Creates the user.
  375. /// </summary>
  376. /// <param name="name">The name.</param>
  377. /// <returns>User.</returns>
  378. /// <exception cref="System.ArgumentNullException">name</exception>
  379. /// <exception cref="System.ArgumentException"></exception>
  380. public async Task<User> CreateUser(string name)
  381. {
  382. if (string.IsNullOrEmpty(name))
  383. {
  384. throw new ArgumentNullException("name");
  385. }
  386. if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
  387. {
  388. throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
  389. }
  390. var user = InstantiateNewUser(name);
  391. var list = Users.ToList();
  392. list.Add(user);
  393. Users = list;
  394. await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
  395. return user;
  396. }
  397. /// <summary>
  398. /// Deletes the user.
  399. /// </summary>
  400. /// <param name="user">The user.</param>
  401. /// <returns>Task.</returns>
  402. /// <exception cref="System.ArgumentNullException">user</exception>
  403. /// <exception cref="System.ArgumentException"></exception>
  404. public async Task DeleteUser(User user)
  405. {
  406. if (user == null)
  407. {
  408. throw new ArgumentNullException("user");
  409. }
  410. if (Users.FirstOrDefault(u => u.Id == user.Id) == null)
  411. {
  412. 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));
  413. }
  414. if (Users.Count() == 1)
  415. {
  416. throw new ArgumentException(string.Format("The user '{0}' be deleted because there must be at least one user in the system.", user.Name));
  417. }
  418. await UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false);
  419. if (user.Configuration.UseCustomLibrary)
  420. {
  421. var path = user.RootFolderPath;
  422. try
  423. {
  424. Directory.Delete(path, true);
  425. }
  426. catch (IOException ex)
  427. {
  428. _logger.ErrorException("Error deleting directory {0}", ex, path);
  429. }
  430. path = user.ConfigurationFilePath;
  431. try
  432. {
  433. File.Delete(path);
  434. }
  435. catch (IOException ex)
  436. {
  437. _logger.ErrorException("Error deleting file {0}", ex, path);
  438. }
  439. }
  440. OnUserDeleted(user);
  441. // Force this to be lazy loaded again
  442. Users = null;
  443. }
  444. /// <summary>
  445. /// Resets the password by clearing it.
  446. /// </summary>
  447. /// <returns>Task.</returns>
  448. public Task ResetPassword(User user)
  449. {
  450. return ChangePassword(user, string.Empty);
  451. }
  452. /// <summary>
  453. /// Changes the password.
  454. /// </summary>
  455. /// <param name="user">The user.</param>
  456. /// <param name="newPassword">The new password.</param>
  457. /// <returns>Task.</returns>
  458. public Task ChangePassword(User user, string newPassword)
  459. {
  460. if (user == null)
  461. {
  462. throw new ArgumentNullException("user");
  463. }
  464. user.Password = string.IsNullOrEmpty(newPassword) ? string.Empty : GetSha1String(newPassword);
  465. return UpdateUser(user);
  466. }
  467. /// <summary>
  468. /// Instantiates the new user.
  469. /// </summary>
  470. /// <param name="name">The name.</param>
  471. /// <returns>User.</returns>
  472. private User InstantiateNewUser(string name)
  473. {
  474. return new User
  475. {
  476. Name = name,
  477. Id = ("MBUser" + name).GetMD5(),
  478. DateCreated = DateTime.UtcNow,
  479. DateModified = DateTime.UtcNow
  480. };
  481. }
  482. /// <summary>
  483. /// Used to report that playback has started for an item
  484. /// </summary>
  485. /// <param name="user">The user.</param>
  486. /// <param name="item">The item.</param>
  487. /// <param name="clientType">Type of the client.</param>
  488. /// <param name="deviceId">The device id.</param>
  489. /// <param name="deviceName">Name of the device.</param>
  490. /// <exception cref="System.ArgumentNullException"></exception>
  491. public void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName)
  492. {
  493. if (user == null)
  494. {
  495. throw new ArgumentNullException();
  496. }
  497. if (item == null)
  498. {
  499. throw new ArgumentNullException();
  500. }
  501. UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item);
  502. // Nothing to save here
  503. // Fire events to inform plugins
  504. EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs
  505. {
  506. Item = item,
  507. User = user
  508. }, _logger);
  509. }
  510. /// <summary>
  511. /// Used to report playback progress for an item
  512. /// </summary>
  513. /// <param name="user">The user.</param>
  514. /// <param name="item">The item.</param>
  515. /// <param name="positionTicks">The position ticks.</param>
  516. /// <param name="clientType">Type of the client.</param>
  517. /// <param name="deviceId">The device id.</param>
  518. /// <param name="deviceName">Name of the device.</param>
  519. /// <returns>Task.</returns>
  520. /// <exception cref="System.ArgumentNullException"></exception>
  521. public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName)
  522. {
  523. if (user == null)
  524. {
  525. throw new ArgumentNullException();
  526. }
  527. if (item == null)
  528. {
  529. throw new ArgumentNullException();
  530. }
  531. UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item, positionTicks);
  532. var key = item.GetUserDataKey();
  533. if (positionTicks.HasValue)
  534. {
  535. var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false);
  536. UpdatePlayState(item, data, positionTicks.Value, false);
  537. await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
  538. }
  539. EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs
  540. {
  541. Item = item,
  542. User = user,
  543. PlaybackPositionTicks = positionTicks
  544. }, _logger);
  545. }
  546. /// <summary>
  547. /// Used to report that playback has ended for an item
  548. /// </summary>
  549. /// <param name="user">The user.</param>
  550. /// <param name="item">The item.</param>
  551. /// <param name="positionTicks">The position ticks.</param>
  552. /// <param name="clientType">Type of the client.</param>
  553. /// <param name="deviceId">The device id.</param>
  554. /// <param name="deviceName">Name of the device.</param>
  555. /// <returns>Task.</returns>
  556. /// <exception cref="System.ArgumentNullException"></exception>
  557. public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName)
  558. {
  559. if (user == null)
  560. {
  561. throw new ArgumentNullException();
  562. }
  563. if (item == null)
  564. {
  565. throw new ArgumentNullException();
  566. }
  567. RemoveNowPlayingItemId(user, clientType, deviceId, deviceName, item);
  568. var key = item.GetUserDataKey();
  569. var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false);
  570. if (positionTicks.HasValue)
  571. {
  572. UpdatePlayState(item, data, positionTicks.Value, true);
  573. }
  574. else
  575. {
  576. // If the client isn't able to report this, then we'll just have to make an assumption
  577. data.PlayCount++;
  578. data.Played = true;
  579. }
  580. await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
  581. EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs
  582. {
  583. Item = item,
  584. User = user,
  585. PlaybackPositionTicks = positionTicks
  586. }, _logger);
  587. }
  588. /// <summary>
  589. /// Updates playstate position for an item but does not save
  590. /// </summary>
  591. /// <param name="item">The item</param>
  592. /// <param name="data">User data for the item</param>
  593. /// <param name="positionTicks">The current playback position</param>
  594. /// <param name="incrementPlayCount">Whether or not to increment playcount</param>
  595. private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount)
  596. {
  597. // If a position has been reported, and if we know the duration
  598. if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0)
  599. {
  600. var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100;
  601. // Don't track in very beginning
  602. if (pctIn < ConfigurationManager.Configuration.MinResumePct)
  603. {
  604. positionTicks = 0;
  605. incrementPlayCount = false;
  606. }
  607. // If we're at the end, assume completed
  608. else if (pctIn > ConfigurationManager.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value)
  609. {
  610. positionTicks = 0;
  611. data.Played = true;
  612. }
  613. else
  614. {
  615. // Enforce MinResumeDuration
  616. var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds;
  617. if (durationSeconds < ConfigurationManager.Configuration.MinResumeDurationSeconds)
  618. {
  619. positionTicks = 0;
  620. data.Played = true;
  621. }
  622. }
  623. }
  624. data.PlaybackPositionTicks = positionTicks;
  625. if (incrementPlayCount)
  626. {
  627. data.PlayCount++;
  628. data.LastPlayedDate = DateTime.UtcNow;
  629. }
  630. }
  631. }
  632. }