SessionManager.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. using MediaBrowser.Common.Events;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Dto;
  5. using MediaBrowser.Controller.Entities;
  6. using MediaBrowser.Controller.Entities.Audio;
  7. using MediaBrowser.Controller.Library;
  8. using MediaBrowser.Controller.Persistence;
  9. using MediaBrowser.Controller.Session;
  10. using MediaBrowser.Model.Logging;
  11. using MediaBrowser.Model.Session;
  12. using System;
  13. using System.Collections.Concurrent;
  14. using System.Collections.Generic;
  15. using System.Linq;
  16. using System.Threading;
  17. using System.Threading.Tasks;
  18. namespace MediaBrowser.Server.Implementations.Session
  19. {
  20. public class SessionManager : ISessionManager
  21. {
  22. private readonly IUserDataRepository _userDataRepository;
  23. private readonly IUserRepository _userRepository;
  24. /// <summary>
  25. /// The _logger
  26. /// </summary>
  27. private readonly ILogger _logger;
  28. /// <summary>
  29. /// Gets or sets the configuration manager.
  30. /// </summary>
  31. /// <value>The configuration manager.</value>
  32. private readonly IServerConfigurationManager _configurationManager;
  33. /// <summary>
  34. /// The _active connections
  35. /// </summary>
  36. private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
  37. new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
  38. private readonly ConcurrentDictionary<Guid, IWebSocketConnection> _websocketConnections =
  39. new ConcurrentDictionary<Guid, IWebSocketConnection>();
  40. /// <summary>
  41. /// Occurs when [playback start].
  42. /// </summary>
  43. public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
  44. /// <summary>
  45. /// Occurs when [playback progress].
  46. /// </summary>
  47. public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
  48. /// <summary>
  49. /// Occurs when [playback stopped].
  50. /// </summary>
  51. public event EventHandler<PlaybackProgressEventArgs> PlaybackStopped;
  52. public SessionManager(IUserDataRepository userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository)
  53. {
  54. _userDataRepository = userDataRepository;
  55. _configurationManager = configurationManager;
  56. _logger = logger;
  57. _userRepository = userRepository;
  58. }
  59. /// <summary>
  60. /// Gets all connections.
  61. /// </summary>
  62. /// <value>All connections.</value>
  63. public IEnumerable<SessionInfo> AllConnections
  64. {
  65. get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); }
  66. }
  67. /// <summary>
  68. /// Gets the active connections.
  69. /// </summary>
  70. /// <value>The active connections.</value>
  71. public IEnumerable<SessionInfo> RecentConnections
  72. {
  73. get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 5); }
  74. }
  75. private readonly Task _trueTaskResult = Task.FromResult(true);
  76. /// <summary>
  77. /// Logs the user activity.
  78. /// </summary>
  79. /// <param name="clientType">Type of the client.</param>
  80. /// <param name="deviceId">The device id.</param>
  81. /// <param name="deviceName">Name of the device.</param>
  82. /// <param name="user">The user.</param>
  83. /// <returns>Task.</returns>
  84. /// <exception cref="System.ArgumentNullException">user</exception>
  85. public Task LogConnectionActivity(string clientType, string deviceId, string deviceName, User user)
  86. {
  87. var activityDate = DateTime.UtcNow;
  88. GetConnection(clientType, deviceId, deviceName, user).LastActivityDate = activityDate;
  89. if (user == null)
  90. {
  91. return _trueTaskResult;
  92. }
  93. var lastActivityDate = user.LastActivityDate;
  94. user.LastActivityDate = activityDate;
  95. // Don't log in the db anymore frequently than 10 seconds
  96. if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10)
  97. {
  98. return _trueTaskResult;
  99. }
  100. // Save this directly. No need to fire off all the events for this.
  101. return _userRepository.SaveUser(user, CancellationToken.None);
  102. }
  103. /// <summary>
  104. /// Updates the now playing item id.
  105. /// </summary>
  106. /// <param name="user">The user.</param>
  107. /// <param name="clientType">Type of the client.</param>
  108. /// <param name="deviceId">The device id.</param>
  109. /// <param name="deviceName">Name of the device.</param>
  110. /// <param name="item">The item.</param>
  111. /// <param name="currentPositionTicks">The current position ticks.</param>
  112. private void UpdateNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item, long? currentPositionTicks = null)
  113. {
  114. var conn = GetConnection(clientType, deviceId, deviceName, user);
  115. conn.NowPlayingPositionTicks = currentPositionTicks;
  116. conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item);
  117. conn.LastActivityDate = DateTime.UtcNow;
  118. }
  119. /// <summary>
  120. /// Removes the now playing item id.
  121. /// </summary>
  122. /// <param name="user">The user.</param>
  123. /// <param name="clientType">Type of the client.</param>
  124. /// <param name="deviceId">The device id.</param>
  125. /// <param name="deviceName">Name of the device.</param>
  126. /// <param name="item">The item.</param>
  127. private void RemoveNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item)
  128. {
  129. var conn = GetConnection(clientType, deviceId, deviceName, user);
  130. if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString()))
  131. {
  132. conn.NowPlayingItem = null;
  133. conn.NowPlayingPositionTicks = null;
  134. }
  135. }
  136. /// <summary>
  137. /// Gets the connection.
  138. /// </summary>
  139. /// <param name="clientType">Type of the client.</param>
  140. /// <param name="deviceId">The device id.</param>
  141. /// <param name="deviceName">Name of the device.</param>
  142. /// <param name="user">The user.</param>
  143. /// <returns>SessionInfo.</returns>
  144. private SessionInfo GetConnection(string clientType, string deviceId, string deviceName, User user)
  145. {
  146. var key = clientType + deviceId;
  147. var connection = _activeConnections.GetOrAdd(key, keyName => new SessionInfo
  148. {
  149. Client = clientType,
  150. DeviceId = deviceId,
  151. Id = Guid.NewGuid()
  152. });
  153. connection.DeviceName = deviceName;
  154. connection.UserId = user == null ? null : user.Id.ToString();
  155. return connection;
  156. }
  157. /// <summary>
  158. /// Used to report that playback has started for an item
  159. /// </summary>
  160. /// <param name="user">The user.</param>
  161. /// <param name="item">The item.</param>
  162. /// <param name="clientType">Type of the client.</param>
  163. /// <param name="deviceId">The device id.</param>
  164. /// <param name="deviceName">Name of the device.</param>
  165. /// <exception cref="System.ArgumentNullException"></exception>
  166. public void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName)
  167. {
  168. if (user == null)
  169. {
  170. throw new ArgumentNullException();
  171. }
  172. if (item == null)
  173. {
  174. throw new ArgumentNullException();
  175. }
  176. UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item);
  177. // Nothing to save here
  178. // Fire events to inform plugins
  179. EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs
  180. {
  181. Item = item,
  182. User = user
  183. }, _logger);
  184. }
  185. /// <summary>
  186. /// Used to report playback progress for an item
  187. /// </summary>
  188. /// <param name="user">The user.</param>
  189. /// <param name="item">The item.</param>
  190. /// <param name="positionTicks">The position ticks.</param>
  191. /// <param name="clientType">Type of the client.</param>
  192. /// <param name="deviceId">The device id.</param>
  193. /// <param name="deviceName">Name of the device.</param>
  194. /// <returns>Task.</returns>
  195. /// <exception cref="System.ArgumentNullException"></exception>
  196. public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName)
  197. {
  198. if (user == null)
  199. {
  200. throw new ArgumentNullException();
  201. }
  202. if (item == null)
  203. {
  204. throw new ArgumentNullException();
  205. }
  206. UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item, positionTicks);
  207. var key = item.GetUserDataKey();
  208. if (positionTicks.HasValue)
  209. {
  210. var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false);
  211. UpdatePlayState(item, data, positionTicks.Value, false);
  212. await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
  213. }
  214. EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs
  215. {
  216. Item = item,
  217. User = user,
  218. PlaybackPositionTicks = positionTicks
  219. }, _logger);
  220. }
  221. /// <summary>
  222. /// Used to report that playback has ended for an item
  223. /// </summary>
  224. /// <param name="user">The user.</param>
  225. /// <param name="item">The item.</param>
  226. /// <param name="positionTicks">The position ticks.</param>
  227. /// <param name="clientType">Type of the client.</param>
  228. /// <param name="deviceId">The device id.</param>
  229. /// <param name="deviceName">Name of the device.</param>
  230. /// <returns>Task.</returns>
  231. /// <exception cref="System.ArgumentNullException"></exception>
  232. public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName)
  233. {
  234. if (user == null)
  235. {
  236. throw new ArgumentNullException();
  237. }
  238. if (item == null)
  239. {
  240. throw new ArgumentNullException();
  241. }
  242. RemoveNowPlayingItemId(user, clientType, deviceId, deviceName, item);
  243. var key = item.GetUserDataKey();
  244. var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false);
  245. if (positionTicks.HasValue)
  246. {
  247. UpdatePlayState(item, data, positionTicks.Value, true);
  248. }
  249. else
  250. {
  251. // If the client isn't able to report this, then we'll just have to make an assumption
  252. data.PlayCount++;
  253. data.Played = true;
  254. }
  255. await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
  256. EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs
  257. {
  258. Item = item,
  259. User = user,
  260. PlaybackPositionTicks = positionTicks
  261. }, _logger);
  262. }
  263. /// <summary>
  264. /// Updates playstate position for an item but does not save
  265. /// </summary>
  266. /// <param name="item">The item</param>
  267. /// <param name="data">User data for the item</param>
  268. /// <param name="positionTicks">The current playback position</param>
  269. /// <param name="incrementPlayCount">Whether or not to increment playcount</param>
  270. private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount)
  271. {
  272. // If a position has been reported, and if we know the duration
  273. if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0)
  274. {
  275. var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100;
  276. // Don't track in very beginning
  277. if (pctIn < _configurationManager.Configuration.MinResumePct)
  278. {
  279. positionTicks = 0;
  280. incrementPlayCount = false;
  281. }
  282. // If we're at the end, assume completed
  283. else if (pctIn > _configurationManager.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value)
  284. {
  285. positionTicks = 0;
  286. data.Played = true;
  287. }
  288. else
  289. {
  290. // Enforce MinResumeDuration
  291. var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds;
  292. if (durationSeconds < _configurationManager.Configuration.MinResumeDurationSeconds)
  293. {
  294. positionTicks = 0;
  295. data.Played = true;
  296. }
  297. }
  298. }
  299. if (item is Audio)
  300. {
  301. data.PlaybackPositionTicks = 0;
  302. }
  303. data.PlaybackPositionTicks = positionTicks;
  304. if (incrementPlayCount)
  305. {
  306. data.PlayCount++;
  307. data.LastPlayedDate = DateTime.UtcNow;
  308. }
  309. }
  310. /// <summary>
  311. /// Identifies the web socket.
  312. /// </summary>
  313. /// <param name="sessionId">The session id.</param>
  314. /// <param name="webSocket">The web socket.</param>
  315. public void IdentifyWebSocket(Guid sessionId, IWebSocketConnection webSocket)
  316. {
  317. _websocketConnections.AddOrUpdate(sessionId, webSocket, (key, existing) => webSocket);
  318. }
  319. }
  320. }