UserManager.cs 25 KB

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