SessionManager.cs 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869
  1. #nullable disable
  2. #pragma warning disable CS1591
  3. using System;
  4. using System.Collections.Concurrent;
  5. using System.Collections.Generic;
  6. using System.Globalization;
  7. using System.Linq;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using Jellyfin.Data.Entities;
  11. using Jellyfin.Data.Entities.Security;
  12. using Jellyfin.Data.Enums;
  13. using Jellyfin.Data.Events;
  14. using Jellyfin.Data.Queries;
  15. using MediaBrowser.Common.Events;
  16. using MediaBrowser.Common.Extensions;
  17. using MediaBrowser.Controller;
  18. using MediaBrowser.Controller.Authentication;
  19. using MediaBrowser.Controller.Devices;
  20. using MediaBrowser.Controller.Drawing;
  21. using MediaBrowser.Controller.Dto;
  22. using MediaBrowser.Controller.Entities;
  23. using MediaBrowser.Controller.Events;
  24. using MediaBrowser.Controller.Events.Session;
  25. using MediaBrowser.Controller.Library;
  26. using MediaBrowser.Controller.Net;
  27. using MediaBrowser.Controller.Session;
  28. using MediaBrowser.Model.Dto;
  29. using MediaBrowser.Model.Entities;
  30. using MediaBrowser.Model.Library;
  31. using MediaBrowser.Model.Querying;
  32. using MediaBrowser.Model.Session;
  33. using MediaBrowser.Model.SyncPlay;
  34. using Microsoft.EntityFrameworkCore;
  35. using Microsoft.Extensions.Logging;
  36. using Episode = MediaBrowser.Controller.Entities.TV.Episode;
  37. namespace Emby.Server.Implementations.Session
  38. {
  39. /// <summary>
  40. /// Class SessionManager.
  41. /// </summary>
  42. public class SessionManager : ISessionManager, IDisposable
  43. {
  44. private readonly IUserDataManager _userDataManager;
  45. private readonly ILogger<SessionManager> _logger;
  46. private readonly IEventManager _eventManager;
  47. private readonly ILibraryManager _libraryManager;
  48. private readonly IUserManager _userManager;
  49. private readonly IMusicManager _musicManager;
  50. private readonly IDtoService _dtoService;
  51. private readonly IImageProcessor _imageProcessor;
  52. private readonly IMediaSourceManager _mediaSourceManager;
  53. private readonly IServerApplicationHost _appHost;
  54. private readonly IDeviceManager _deviceManager;
  55. /// <summary>
  56. /// The active connections.
  57. /// </summary>
  58. private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new (StringComparer.OrdinalIgnoreCase);
  59. private Timer _idleTimer;
  60. private DtoOptions _itemInfoDtoOptions;
  61. private bool _disposed = false;
  62. public SessionManager(
  63. ILogger<SessionManager> logger,
  64. IEventManager eventManager,
  65. IUserDataManager userDataManager,
  66. ILibraryManager libraryManager,
  67. IUserManager userManager,
  68. IMusicManager musicManager,
  69. IDtoService dtoService,
  70. IImageProcessor imageProcessor,
  71. IServerApplicationHost appHost,
  72. IDeviceManager deviceManager,
  73. IMediaSourceManager mediaSourceManager)
  74. {
  75. _logger = logger;
  76. _eventManager = eventManager;
  77. _userDataManager = userDataManager;
  78. _libraryManager = libraryManager;
  79. _userManager = userManager;
  80. _musicManager = musicManager;
  81. _dtoService = dtoService;
  82. _imageProcessor = imageProcessor;
  83. _appHost = appHost;
  84. _deviceManager = deviceManager;
  85. _mediaSourceManager = mediaSourceManager;
  86. _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
  87. }
  88. /// <inheritdoc />
  89. public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
  90. /// <inheritdoc />
  91. public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
  92. /// <summary>
  93. /// Occurs when playback has started.
  94. /// </summary>
  95. public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
  96. /// <summary>
  97. /// Occurs when playback has progressed.
  98. /// </summary>
  99. public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
  100. /// <summary>
  101. /// Occurs when playback has stopped.
  102. /// </summary>
  103. public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
  104. /// <inheritdoc />
  105. public event EventHandler<SessionEventArgs> SessionStarted;
  106. /// <inheritdoc />
  107. public event EventHandler<SessionEventArgs> CapabilitiesChanged;
  108. /// <inheritdoc />
  109. public event EventHandler<SessionEventArgs> SessionEnded;
  110. /// <inheritdoc />
  111. public event EventHandler<SessionEventArgs> SessionActivity;
  112. /// <inheritdoc />
  113. public event EventHandler<SessionEventArgs> SessionControllerConnected;
  114. /// <summary>
  115. /// Gets all connections.
  116. /// </summary>
  117. /// <value>All connections.</value>
  118. public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
  119. private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e)
  120. {
  121. foreach (var session in Sessions)
  122. {
  123. if (string.Equals(session.DeviceId, e.Argument.Item1, StringComparison.Ordinal))
  124. {
  125. if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName))
  126. {
  127. session.HasCustomDeviceName = true;
  128. session.DeviceName = e.Argument.Item2.CustomName;
  129. }
  130. else
  131. {
  132. session.HasCustomDeviceName = false;
  133. }
  134. }
  135. }
  136. }
  137. /// <inheritdoc />
  138. public void Dispose()
  139. {
  140. Dispose(true);
  141. GC.SuppressFinalize(this);
  142. }
  143. /// <summary>
  144. /// Releases unmanaged and optionally managed resources.
  145. /// </summary>
  146. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  147. protected virtual void Dispose(bool disposing)
  148. {
  149. if (_disposed)
  150. {
  151. return;
  152. }
  153. if (disposing)
  154. {
  155. _idleTimer?.Dispose();
  156. }
  157. _idleTimer = null;
  158. _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
  159. _disposed = true;
  160. }
  161. private void CheckDisposed()
  162. {
  163. if (_disposed)
  164. {
  165. throw new ObjectDisposedException(GetType().Name);
  166. }
  167. }
  168. private void OnSessionStarted(SessionInfo info)
  169. {
  170. if (!string.IsNullOrEmpty(info.DeviceId))
  171. {
  172. var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
  173. if (capabilities != null)
  174. {
  175. ReportCapabilities(info, capabilities, false);
  176. }
  177. }
  178. _eventManager.Publish(new SessionStartedEventArgs(info));
  179. EventHelper.QueueEventIfNotNull(
  180. SessionStarted,
  181. this,
  182. new SessionEventArgs
  183. {
  184. SessionInfo = info
  185. },
  186. _logger);
  187. }
  188. private void OnSessionEnded(SessionInfo info)
  189. {
  190. EventHelper.QueueEventIfNotNull(
  191. SessionEnded,
  192. this,
  193. new SessionEventArgs
  194. {
  195. SessionInfo = info
  196. },
  197. _logger);
  198. _eventManager.Publish(new SessionEndedEventArgs(info));
  199. info.Dispose();
  200. }
  201. /// <inheritdoc />
  202. public void UpdateDeviceName(string sessionId, string deviceName)
  203. {
  204. var session = GetSession(sessionId);
  205. if (session != null)
  206. {
  207. session.DeviceName = deviceName;
  208. }
  209. }
  210. /// <summary>
  211. /// Logs the user activity.
  212. /// </summary>
  213. /// <param name="appName">Type of the client.</param>
  214. /// <param name="appVersion">The app version.</param>
  215. /// <param name="deviceId">The device id.</param>
  216. /// <param name="deviceName">Name of the device.</param>
  217. /// <param name="remoteEndPoint">The remote end point.</param>
  218. /// <param name="user">The user.</param>
  219. /// <returns>SessionInfo.</returns>
  220. public async Task<SessionInfo> LogSessionActivity(
  221. string appName,
  222. string appVersion,
  223. string deviceId,
  224. string deviceName,
  225. string remoteEndPoint,
  226. User user)
  227. {
  228. CheckDisposed();
  229. if (string.IsNullOrEmpty(appName))
  230. {
  231. throw new ArgumentNullException(nameof(appName));
  232. }
  233. if (string.IsNullOrEmpty(appVersion))
  234. {
  235. throw new ArgumentNullException(nameof(appVersion));
  236. }
  237. if (string.IsNullOrEmpty(deviceId))
  238. {
  239. throw new ArgumentNullException(nameof(deviceId));
  240. }
  241. var activityDate = DateTime.UtcNow;
  242. var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
  243. var lastActivityDate = session.LastActivityDate;
  244. session.LastActivityDate = activityDate;
  245. if (user != null)
  246. {
  247. var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
  248. if ((activityDate - userLastActivityDate).TotalSeconds > 60)
  249. {
  250. try
  251. {
  252. user.LastActivityDate = activityDate;
  253. await _userManager.UpdateUserAsync(user);
  254. }
  255. catch (DbUpdateConcurrencyException e)
  256. {
  257. _logger.LogDebug(e, "Error updating user's last activity date.");
  258. }
  259. }
  260. }
  261. if ((activityDate - lastActivityDate).TotalSeconds > 10)
  262. {
  263. SessionActivity?.Invoke(
  264. this,
  265. new SessionEventArgs
  266. {
  267. SessionInfo = session
  268. });
  269. }
  270. return session;
  271. }
  272. /// <inheritdoc />
  273. public void OnSessionControllerConnected(SessionInfo info)
  274. {
  275. EventHelper.QueueEventIfNotNull(
  276. SessionControllerConnected,
  277. this,
  278. new SessionEventArgs
  279. {
  280. SessionInfo = info
  281. },
  282. _logger);
  283. }
  284. /// <inheritdoc />
  285. public void CloseIfNeeded(SessionInfo session)
  286. {
  287. if (!session.SessionControllers.Any(i => i.IsSessionActive))
  288. {
  289. var key = GetSessionKey(session.Client, session.DeviceId);
  290. _activeConnections.TryRemove(key, out _);
  291. OnSessionEnded(session);
  292. }
  293. }
  294. /// <inheritdoc />
  295. public void ReportSessionEnded(string sessionId)
  296. {
  297. CheckDisposed();
  298. var session = GetSession(sessionId, false);
  299. if (session != null)
  300. {
  301. var key = GetSessionKey(session.Client, session.DeviceId);
  302. _activeConnections.TryRemove(key, out _);
  303. OnSessionEnded(session);
  304. }
  305. }
  306. private Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId)
  307. {
  308. return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None);
  309. }
  310. /// <summary>
  311. /// Updates the now playing item id.
  312. /// </summary>
  313. /// <returns>Task.</returns>
  314. private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime)
  315. {
  316. if (string.IsNullOrEmpty(info.MediaSourceId))
  317. {
  318. info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
  319. }
  320. if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null)
  321. {
  322. var current = session.NowPlayingItem;
  323. if (current == null || !info.ItemId.Equals(current.Id))
  324. {
  325. var runtimeTicks = libraryItem.RunTimeTicks;
  326. MediaSourceInfo mediaSource = null;
  327. if (libraryItem is IHasMediaSources)
  328. {
  329. mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
  330. if (mediaSource != null)
  331. {
  332. runtimeTicks = mediaSource.RunTimeTicks;
  333. }
  334. }
  335. info.Item = GetItemInfo(libraryItem, mediaSource);
  336. info.Item.RunTimeTicks = runtimeTicks;
  337. }
  338. else
  339. {
  340. info.Item = current;
  341. }
  342. }
  343. session.NowPlayingItem = info.Item;
  344. session.LastActivityDate = DateTime.UtcNow;
  345. if (updateLastCheckInTime)
  346. {
  347. session.LastPlaybackCheckIn = DateTime.UtcNow;
  348. }
  349. session.PlayState.IsPaused = info.IsPaused;
  350. session.PlayState.PositionTicks = info.PositionTicks;
  351. session.PlayState.MediaSourceId = info.MediaSourceId;
  352. session.PlayState.CanSeek = info.CanSeek;
  353. session.PlayState.IsMuted = info.IsMuted;
  354. session.PlayState.VolumeLevel = info.VolumeLevel;
  355. session.PlayState.AudioStreamIndex = info.AudioStreamIndex;
  356. session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex;
  357. session.PlayState.PlayMethod = info.PlayMethod;
  358. session.PlayState.RepeatMode = info.RepeatMode;
  359. session.PlaylistItemId = info.PlaylistItemId;
  360. var nowPlayingQueue = info.NowPlayingQueue;
  361. if (nowPlayingQueue != null)
  362. {
  363. session.NowPlayingQueue = nowPlayingQueue;
  364. }
  365. }
  366. /// <summary>
  367. /// Removes the now playing item id.
  368. /// </summary>
  369. /// <param name="session">The session.</param>
  370. private void RemoveNowPlayingItem(SessionInfo session)
  371. {
  372. session.NowPlayingItem = null;
  373. session.PlayState = new PlayerStateInfo();
  374. if (!string.IsNullOrEmpty(session.DeviceId))
  375. {
  376. ClearTranscodingInfo(session.DeviceId);
  377. }
  378. }
  379. private static string GetSessionKey(string appName, string deviceId)
  380. => appName + deviceId;
  381. /// <summary>
  382. /// Gets the connection.
  383. /// </summary>
  384. /// <param name="appName">Type of the client.</param>
  385. /// <param name="appVersion">The app version.</param>
  386. /// <param name="deviceId">The device id.</param>
  387. /// <param name="deviceName">Name of the device.</param>
  388. /// <param name="remoteEndPoint">The remote end point.</param>
  389. /// <param name="user">The user.</param>
  390. /// <returns>SessionInfo.</returns>
  391. private async Task<SessionInfo> GetSessionInfo(
  392. string appName,
  393. string appVersion,
  394. string deviceId,
  395. string deviceName,
  396. string remoteEndPoint,
  397. User user)
  398. {
  399. CheckDisposed();
  400. if (string.IsNullOrEmpty(deviceId))
  401. {
  402. throw new ArgumentNullException(nameof(deviceId));
  403. }
  404. var key = GetSessionKey(appName, deviceId);
  405. CheckDisposed();
  406. if (!_activeConnections.TryGetValue(key, out var sessionInfo))
  407. {
  408. _activeConnections[key] = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
  409. sessionInfo = _activeConnections[key];
  410. }
  411. sessionInfo.UserId = user?.Id ?? Guid.Empty;
  412. sessionInfo.UserName = user?.Username;
  413. sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
  414. sessionInfo.RemoteEndPoint = remoteEndPoint;
  415. sessionInfo.Client = appName;
  416. if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
  417. {
  418. sessionInfo.DeviceName = deviceName;
  419. }
  420. sessionInfo.ApplicationVersion = appVersion;
  421. if (user == null)
  422. {
  423. sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
  424. }
  425. return sessionInfo;
  426. }
  427. private async Task<SessionInfo> CreateSession(
  428. string key,
  429. string appName,
  430. string appVersion,
  431. string deviceId,
  432. string deviceName,
  433. string remoteEndPoint,
  434. User user)
  435. {
  436. var sessionInfo = new SessionInfo(this, _logger)
  437. {
  438. Client = appName,
  439. DeviceId = deviceId,
  440. ApplicationVersion = appVersion,
  441. Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
  442. ServerId = _appHost.SystemId
  443. };
  444. var username = user?.Username;
  445. sessionInfo.UserId = user?.Id ?? Guid.Empty;
  446. sessionInfo.UserName = username;
  447. sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
  448. sessionInfo.RemoteEndPoint = remoteEndPoint;
  449. if (string.IsNullOrEmpty(deviceName))
  450. {
  451. deviceName = "Network Device";
  452. }
  453. var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
  454. if (string.IsNullOrEmpty(deviceOptions?.CustomName))
  455. {
  456. sessionInfo.DeviceName = deviceName;
  457. }
  458. else
  459. {
  460. sessionInfo.DeviceName = deviceOptions.CustomName;
  461. sessionInfo.HasCustomDeviceName = true;
  462. }
  463. OnSessionStarted(sessionInfo);
  464. return sessionInfo;
  465. }
  466. private List<User> GetUsers(SessionInfo session)
  467. {
  468. var users = new List<User>();
  469. if (session.UserId != Guid.Empty)
  470. {
  471. var user = _userManager.GetUserById(session.UserId);
  472. if (user == null)
  473. {
  474. throw new InvalidOperationException("User not found");
  475. }
  476. users.Add(user);
  477. users.AddRange(session.AdditionalUsers
  478. .Select(i => _userManager.GetUserById(i.UserId))
  479. .Where(i => i != null));
  480. }
  481. return users;
  482. }
  483. private void StartIdleCheckTimer()
  484. {
  485. _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
  486. }
  487. private void StopIdleCheckTimer()
  488. {
  489. if (_idleTimer != null)
  490. {
  491. _idleTimer.Dispose();
  492. _idleTimer = null;
  493. }
  494. }
  495. private async void CheckForIdlePlayback(object state)
  496. {
  497. var playingSessions = Sessions.Where(i => i.NowPlayingItem != null)
  498. .ToList();
  499. if (playingSessions.Count > 0)
  500. {
  501. var idle = playingSessions
  502. .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
  503. .ToList();
  504. foreach (var session in idle)
  505. {
  506. _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
  507. try
  508. {
  509. await OnPlaybackStopped(new PlaybackStopInfo
  510. {
  511. Item = session.NowPlayingItem,
  512. ItemId = session.NowPlayingItem == null ? Guid.Empty : session.NowPlayingItem.Id,
  513. SessionId = session.Id,
  514. MediaSourceId = session.PlayState?.MediaSourceId,
  515. PositionTicks = session.PlayState?.PositionTicks
  516. }).ConfigureAwait(false);
  517. }
  518. catch (Exception ex)
  519. {
  520. _logger.LogDebug("Error calling OnPlaybackStopped", ex);
  521. }
  522. }
  523. playingSessions = Sessions.Where(i => i.NowPlayingItem != null)
  524. .ToList();
  525. }
  526. if (playingSessions.Count == 0)
  527. {
  528. StopIdleCheckTimer();
  529. }
  530. }
  531. private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
  532. {
  533. var item = session.FullNowPlayingItem;
  534. if (item != null && item.Id.Equals(itemId))
  535. {
  536. return item;
  537. }
  538. item = _libraryManager.GetItemById(itemId);
  539. session.FullNowPlayingItem = item;
  540. return item;
  541. }
  542. /// <summary>
  543. /// Used to report that playback has started for an item.
  544. /// </summary>
  545. /// <param name="info">The info.</param>
  546. /// <returns>Task.</returns>
  547. /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
  548. public async Task OnPlaybackStart(PlaybackStartInfo info)
  549. {
  550. CheckDisposed();
  551. if (info == null)
  552. {
  553. throw new ArgumentNullException(nameof(info));
  554. }
  555. var session = GetSession(info.SessionId);
  556. var libraryItem = info.ItemId == Guid.Empty
  557. ? null
  558. : GetNowPlayingItem(session, info.ItemId);
  559. await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
  560. if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
  561. {
  562. ClearTranscodingInfo(session.DeviceId);
  563. }
  564. session.StartAutomaticProgress(info);
  565. var users = GetUsers(session);
  566. if (libraryItem != null)
  567. {
  568. foreach (var user in users)
  569. {
  570. OnPlaybackStart(user, libraryItem);
  571. }
  572. }
  573. var eventArgs = new PlaybackStartEventArgs
  574. {
  575. Item = libraryItem,
  576. Users = users,
  577. MediaSourceId = info.MediaSourceId,
  578. MediaInfo = info.Item,
  579. DeviceName = session.DeviceName,
  580. ClientName = session.Client,
  581. DeviceId = session.DeviceId,
  582. Session = session
  583. };
  584. await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
  585. // Nothing to save here
  586. // Fire events to inform plugins
  587. EventHelper.QueueEventIfNotNull(
  588. PlaybackStart,
  589. this,
  590. eventArgs,
  591. _logger);
  592. StartIdleCheckTimer();
  593. }
  594. /// <summary>
  595. /// Called when [playback start].
  596. /// </summary>
  597. /// <param name="user">The user object.</param>
  598. /// <param name="item">The item.</param>
  599. private void OnPlaybackStart(User user, BaseItem item)
  600. {
  601. var data = _userDataManager.GetUserData(user, item);
  602. data.PlayCount++;
  603. data.LastPlayedDate = DateTime.UtcNow;
  604. if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
  605. {
  606. data.Played = true;
  607. }
  608. else
  609. {
  610. data.Played = false;
  611. }
  612. _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
  613. }
  614. /// <inheritdoc />
  615. public Task OnPlaybackProgress(PlaybackProgressInfo info)
  616. {
  617. return OnPlaybackProgress(info, false);
  618. }
  619. /// <summary>
  620. /// Used to report playback progress for an item.
  621. /// </summary>
  622. /// <returns>Task.</returns>
  623. public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
  624. {
  625. CheckDisposed();
  626. if (info == null)
  627. {
  628. throw new ArgumentNullException(nameof(info));
  629. }
  630. var session = GetSession(info.SessionId);
  631. var libraryItem = info.ItemId.Equals(Guid.Empty)
  632. ? null
  633. : GetNowPlayingItem(session, info.ItemId);
  634. await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
  635. var users = GetUsers(session);
  636. // only update saved user data on actual check-ins, not automated ones
  637. if (libraryItem != null && !isAutomated)
  638. {
  639. foreach (var user in users)
  640. {
  641. OnPlaybackProgress(user, libraryItem, info);
  642. }
  643. }
  644. var eventArgs = new PlaybackProgressEventArgs
  645. {
  646. Item = libraryItem,
  647. Users = users,
  648. PlaybackPositionTicks = session.PlayState.PositionTicks,
  649. MediaSourceId = session.PlayState.MediaSourceId,
  650. MediaInfo = info.Item,
  651. DeviceName = session.DeviceName,
  652. ClientName = session.Client,
  653. DeviceId = session.DeviceId,
  654. IsPaused = info.IsPaused,
  655. PlaySessionId = info.PlaySessionId,
  656. IsAutomated = isAutomated,
  657. Session = session
  658. };
  659. await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
  660. PlaybackProgress?.Invoke(this, eventArgs);
  661. if (!isAutomated)
  662. {
  663. session.StartAutomaticProgress(info);
  664. }
  665. StartIdleCheckTimer();
  666. }
  667. private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
  668. {
  669. var data = _userDataManager.GetUserData(user, item);
  670. var positionTicks = info.PositionTicks;
  671. var changed = false;
  672. if (positionTicks.HasValue)
  673. {
  674. _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
  675. changed = true;
  676. }
  677. var tracksChanged = UpdatePlaybackSettings(user, info, data);
  678. if (!tracksChanged)
  679. {
  680. changed = true;
  681. }
  682. if (changed)
  683. {
  684. _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None);
  685. }
  686. }
  687. private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
  688. {
  689. var changed = false;
  690. if (user.RememberAudioSelections)
  691. {
  692. if (data.AudioStreamIndex != info.AudioStreamIndex)
  693. {
  694. data.AudioStreamIndex = info.AudioStreamIndex;
  695. changed = true;
  696. }
  697. }
  698. else
  699. {
  700. if (data.AudioStreamIndex.HasValue)
  701. {
  702. data.AudioStreamIndex = null;
  703. changed = true;
  704. }
  705. }
  706. if (user.RememberSubtitleSelections)
  707. {
  708. if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
  709. {
  710. data.SubtitleStreamIndex = info.SubtitleStreamIndex;
  711. changed = true;
  712. }
  713. }
  714. else
  715. {
  716. if (data.SubtitleStreamIndex.HasValue)
  717. {
  718. data.SubtitleStreamIndex = null;
  719. changed = true;
  720. }
  721. }
  722. return changed;
  723. }
  724. /// <summary>
  725. /// Used to report that playback has ended for an item.
  726. /// </summary>
  727. /// <param name="info">The info.</param>
  728. /// <returns>Task.</returns>
  729. /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
  730. /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
  731. public async Task OnPlaybackStopped(PlaybackStopInfo info)
  732. {
  733. CheckDisposed();
  734. if (info == null)
  735. {
  736. throw new ArgumentNullException(nameof(info));
  737. }
  738. if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
  739. {
  740. throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative.");
  741. }
  742. var session = GetSession(info.SessionId);
  743. session.StopAutomaticProgress();
  744. var libraryItem = info.ItemId.Equals(Guid.Empty)
  745. ? null
  746. : GetNowPlayingItem(session, info.ItemId);
  747. // Normalize
  748. if (string.IsNullOrEmpty(info.MediaSourceId))
  749. {
  750. info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
  751. }
  752. if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null)
  753. {
  754. var current = session.NowPlayingItem;
  755. if (current == null || !info.ItemId.Equals(current.Id))
  756. {
  757. MediaSourceInfo mediaSource = null;
  758. if (libraryItem is IHasMediaSources)
  759. {
  760. mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
  761. }
  762. info.Item = GetItemInfo(libraryItem, mediaSource);
  763. }
  764. else
  765. {
  766. info.Item = current;
  767. }
  768. }
  769. if (info.Item != null)
  770. {
  771. var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.InvariantCulture) : "unknown";
  772. _logger.LogInformation(
  773. "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms",
  774. session.Client,
  775. session.ApplicationVersion,
  776. info.Item.Name,
  777. msString);
  778. }
  779. if (info.NowPlayingQueue != null)
  780. {
  781. session.NowPlayingQueue = info.NowPlayingQueue;
  782. }
  783. session.PlaylistItemId = info.PlaylistItemId;
  784. RemoveNowPlayingItem(session);
  785. var users = GetUsers(session);
  786. var playedToCompletion = false;
  787. if (libraryItem != null)
  788. {
  789. foreach (var user in users)
  790. {
  791. playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
  792. }
  793. }
  794. if (!string.IsNullOrEmpty(info.LiveStreamId))
  795. {
  796. try
  797. {
  798. await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false);
  799. }
  800. catch (Exception ex)
  801. {
  802. _logger.LogError("Error closing live stream", ex);
  803. }
  804. }
  805. var eventArgs = new PlaybackStopEventArgs
  806. {
  807. Item = libraryItem,
  808. Users = users,
  809. PlaybackPositionTicks = info.PositionTicks,
  810. PlayedToCompletion = playedToCompletion,
  811. MediaSourceId = info.MediaSourceId,
  812. MediaInfo = info.Item,
  813. DeviceName = session.DeviceName,
  814. ClientName = session.Client,
  815. DeviceId = session.DeviceId,
  816. Session = session
  817. };
  818. await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
  819. EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
  820. }
  821. private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
  822. {
  823. bool playedToCompletion = false;
  824. if (!playbackFailed)
  825. {
  826. var data = _userDataManager.GetUserData(user, item);
  827. if (positionTicks.HasValue)
  828. {
  829. playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
  830. }
  831. else
  832. {
  833. // If the client isn't able to report this, then we'll just have to make an assumption
  834. data.PlayCount++;
  835. data.Played = item.SupportsPlayedStatus;
  836. data.PlaybackPositionTicks = 0;
  837. playedToCompletion = true;
  838. }
  839. _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
  840. }
  841. return playedToCompletion;
  842. }
  843. /// <summary>
  844. /// Gets the session.
  845. /// </summary>
  846. /// <param name="sessionId">The session identifier.</param>
  847. /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
  848. /// <returns>SessionInfo.</returns>
  849. /// <exception cref="ResourceNotFoundException">
  850. /// No session with an Id equal to <c>sessionId</c> was found
  851. /// and <c>throwOnMissing</c> is <c>true</c>.
  852. /// </exception>
  853. private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
  854. {
  855. var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
  856. if (session == null && throwOnMissing)
  857. {
  858. throw new ResourceNotFoundException(
  859. string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
  860. }
  861. return session;
  862. }
  863. private SessionInfo GetSessionToRemoteControl(string sessionId)
  864. {
  865. // Accept either device id or session id
  866. var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
  867. if (session == null)
  868. {
  869. throw new ResourceNotFoundException(
  870. string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
  871. }
  872. return session;
  873. }
  874. /// <inheritdoc />
  875. public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
  876. {
  877. CheckDisposed();
  878. var generalCommand = new GeneralCommand
  879. {
  880. Name = GeneralCommandType.DisplayMessage
  881. };
  882. generalCommand.Arguments["Header"] = command.Header;
  883. generalCommand.Arguments["Text"] = command.Text;
  884. if (command.TimeoutMs.HasValue)
  885. {
  886. generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
  887. }
  888. return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
  889. }
  890. /// <inheritdoc />
  891. public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken)
  892. {
  893. CheckDisposed();
  894. var session = GetSessionToRemoteControl(sessionId);
  895. if (!string.IsNullOrEmpty(controllingSessionId))
  896. {
  897. var controllingSession = GetSession(controllingSessionId);
  898. AssertCanControl(session, controllingSession);
  899. }
  900. return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
  901. }
  902. private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken)
  903. {
  904. var controllers = session.SessionControllers;
  905. var messageId = Guid.NewGuid();
  906. foreach (var controller in controllers)
  907. {
  908. await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
  909. }
  910. }
  911. private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
  912. {
  913. IEnumerable<Task> GetTasks()
  914. {
  915. var messageId = Guid.NewGuid();
  916. foreach (var session in sessions)
  917. {
  918. var controllers = session.SessionControllers;
  919. foreach (var controller in controllers)
  920. {
  921. yield return controller.SendMessage(name, messageId, data, cancellationToken);
  922. }
  923. }
  924. }
  925. return Task.WhenAll(GetTasks());
  926. }
  927. /// <inheritdoc />
  928. public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
  929. {
  930. CheckDisposed();
  931. var session = GetSessionToRemoteControl(sessionId);
  932. var user = session.UserId == Guid.Empty ? null : _userManager.GetUserById(session.UserId);
  933. List<BaseItem> items;
  934. if (command.PlayCommand == PlayCommand.PlayInstantMix)
  935. {
  936. items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
  937. .ToList();
  938. command.PlayCommand = PlayCommand.PlayNow;
  939. }
  940. else
  941. {
  942. var list = new List<BaseItem>();
  943. foreach (var itemId in command.ItemIds)
  944. {
  945. var subItems = TranslateItemForPlayback(itemId, user);
  946. list.AddRange(subItems);
  947. }
  948. items = list;
  949. }
  950. if (command.PlayCommand == PlayCommand.PlayShuffle)
  951. {
  952. items.Shuffle();
  953. command.PlayCommand = PlayCommand.PlayNow;
  954. }
  955. command.ItemIds = items.Select(i => i.Id).ToArray();
  956. if (user != null)
  957. {
  958. if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
  959. {
  960. throw new ArgumentException(
  961. string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username));
  962. }
  963. }
  964. if (user != null
  965. && command.ItemIds.Length == 1
  966. && user.EnableNextEpisodeAutoPlay
  967. && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
  968. {
  969. var series = episode.Series;
  970. if (series != null)
  971. {
  972. var episodes = series.GetEpisodes(
  973. user,
  974. new DtoOptions(false)
  975. {
  976. EnableImages = false
  977. })
  978. .Where(i => !i.IsVirtualItem)
  979. .SkipWhile(i => i.Id != episode.Id)
  980. .ToList();
  981. if (episodes.Count > 0)
  982. {
  983. command.ItemIds = episodes.Select(i => i.Id).ToArray();
  984. }
  985. }
  986. }
  987. if (!string.IsNullOrEmpty(controllingSessionId))
  988. {
  989. var controllingSession = GetSession(controllingSessionId);
  990. AssertCanControl(session, controllingSession);
  991. if (!controllingSession.UserId.Equals(Guid.Empty))
  992. {
  993. command.ControllingUserId = controllingSession.UserId;
  994. }
  995. }
  996. await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false);
  997. }
  998. /// <inheritdoc />
  999. public async Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken)
  1000. {
  1001. CheckDisposed();
  1002. await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
  1003. }
  1004. /// <inheritdoc />
  1005. public async Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken)
  1006. {
  1007. CheckDisposed();
  1008. await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
  1009. }
  1010. private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
  1011. {
  1012. var item = _libraryManager.GetItemById(id);
  1013. if (item == null)
  1014. {
  1015. _logger.LogError("A non-existant item Id {0} was passed into TranslateItemForPlayback", id);
  1016. return Array.Empty<BaseItem>();
  1017. }
  1018. if (item is IItemByName byName)
  1019. {
  1020. return byName.GetTaggedItems(new InternalItemsQuery(user)
  1021. {
  1022. IsFolder = false,
  1023. Recursive = true,
  1024. DtoOptions = new DtoOptions(false)
  1025. {
  1026. EnableImages = false,
  1027. Fields = new[]
  1028. {
  1029. ItemFields.SortName
  1030. }
  1031. },
  1032. IsVirtualItem = false,
  1033. OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
  1034. });
  1035. }
  1036. if (item.IsFolder)
  1037. {
  1038. var folder = (Folder)item;
  1039. return folder.GetItemList(new InternalItemsQuery(user)
  1040. {
  1041. Recursive = true,
  1042. IsFolder = false,
  1043. DtoOptions = new DtoOptions(false)
  1044. {
  1045. EnableImages = false,
  1046. Fields = new ItemFields[]
  1047. {
  1048. ItemFields.SortName
  1049. }
  1050. },
  1051. IsVirtualItem = false,
  1052. OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
  1053. });
  1054. }
  1055. return new[] { item };
  1056. }
  1057. private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
  1058. {
  1059. var item = _libraryManager.GetItemById(id);
  1060. if (item == null)
  1061. {
  1062. _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id);
  1063. return new List<BaseItem>();
  1064. }
  1065. return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
  1066. }
  1067. /// <inheritdoc />
  1068. public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken)
  1069. {
  1070. var generalCommand = new GeneralCommand
  1071. {
  1072. Name = GeneralCommandType.DisplayContent,
  1073. Arguments =
  1074. {
  1075. ["ItemId"] = command.ItemId,
  1076. ["ItemName"] = command.ItemName,
  1077. ["ItemType"] = command.ItemType
  1078. }
  1079. };
  1080. return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
  1081. }
  1082. /// <inheritdoc />
  1083. public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken)
  1084. {
  1085. CheckDisposed();
  1086. var session = GetSessionToRemoteControl(sessionId);
  1087. if (!string.IsNullOrEmpty(controllingSessionId))
  1088. {
  1089. var controllingSession = GetSession(controllingSessionId);
  1090. AssertCanControl(session, controllingSession);
  1091. if (!controllingSession.UserId.Equals(Guid.Empty))
  1092. {
  1093. command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
  1094. }
  1095. }
  1096. return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
  1097. }
  1098. private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
  1099. {
  1100. if (session == null)
  1101. {
  1102. throw new ArgumentNullException(nameof(session));
  1103. }
  1104. if (controllingSession == null)
  1105. {
  1106. throw new ArgumentNullException(nameof(controllingSession));
  1107. }
  1108. }
  1109. /// <summary>
  1110. /// Sends the restart required message.
  1111. /// </summary>
  1112. /// <param name="cancellationToken">The cancellation token.</param>
  1113. /// <returns>Task.</returns>
  1114. public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
  1115. {
  1116. CheckDisposed();
  1117. return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
  1118. }
  1119. /// <summary>
  1120. /// Sends the server shutdown notification.
  1121. /// </summary>
  1122. /// <param name="cancellationToken">The cancellation token.</param>
  1123. /// <returns>Task.</returns>
  1124. public Task SendServerShutdownNotification(CancellationToken cancellationToken)
  1125. {
  1126. CheckDisposed();
  1127. return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
  1128. }
  1129. /// <summary>
  1130. /// Sends the server restart notification.
  1131. /// </summary>
  1132. /// <param name="cancellationToken">The cancellation token.</param>
  1133. /// <returns>Task.</returns>
  1134. public Task SendServerRestartNotification(CancellationToken cancellationToken)
  1135. {
  1136. CheckDisposed();
  1137. _logger.LogDebug("Beginning SendServerRestartNotification");
  1138. return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
  1139. }
  1140. /// <summary>
  1141. /// Adds the additional user.
  1142. /// </summary>
  1143. /// <param name="sessionId">The session identifier.</param>
  1144. /// <param name="userId">The user identifier.</param>
  1145. /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception>
  1146. /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exception>
  1147. public void AddAdditionalUser(string sessionId, Guid userId)
  1148. {
  1149. CheckDisposed();
  1150. var session = GetSession(sessionId);
  1151. if (session.UserId == userId)
  1152. {
  1153. throw new ArgumentException("The requested user is already the primary user of the session.");
  1154. }
  1155. if (session.AdditionalUsers.All(i => i.UserId != userId))
  1156. {
  1157. var user = _userManager.GetUserById(userId);
  1158. var list = session.AdditionalUsers.ToList();
  1159. list.Add(new SessionUserInfo
  1160. {
  1161. UserId = userId,
  1162. UserName = user.Username
  1163. });
  1164. session.AdditionalUsers = list.ToArray();
  1165. }
  1166. }
  1167. /// <summary>
  1168. /// Removes the additional user.
  1169. /// </summary>
  1170. /// <param name="sessionId">The session identifier.</param>
  1171. /// <param name="userId">The user identifier.</param>
  1172. /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception>
  1173. /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exception>
  1174. public void RemoveAdditionalUser(string sessionId, Guid userId)
  1175. {
  1176. CheckDisposed();
  1177. var session = GetSession(sessionId);
  1178. if (session.UserId.Equals(userId))
  1179. {
  1180. throw new ArgumentException("The requested user is already the primary user of the session.");
  1181. }
  1182. var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
  1183. if (user != null)
  1184. {
  1185. var list = session.AdditionalUsers.ToList();
  1186. list.Remove(user);
  1187. session.AdditionalUsers = list.ToArray();
  1188. }
  1189. }
  1190. /// <summary>
  1191. /// Authenticates the new session.
  1192. /// </summary>
  1193. /// <param name="request">The request.</param>
  1194. /// <returns>Task{SessionInfo}.</returns>
  1195. public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
  1196. {
  1197. return AuthenticateNewSessionInternal(request, true);
  1198. }
  1199. public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
  1200. {
  1201. return AuthenticateNewSessionInternal(request, false);
  1202. }
  1203. private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
  1204. {
  1205. CheckDisposed();
  1206. User user = null;
  1207. if (request.UserId != Guid.Empty)
  1208. {
  1209. user = _userManager.GetUserById(request.UserId);
  1210. }
  1211. user ??= _userManager.GetUserByName(request.Username);
  1212. if (enforcePassword)
  1213. {
  1214. user = await _userManager.AuthenticateUser(
  1215. request.Username,
  1216. request.Password,
  1217. null,
  1218. request.RemoteEndPoint,
  1219. true).ConfigureAwait(false);
  1220. }
  1221. if (user == null)
  1222. {
  1223. AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request));
  1224. throw new AuthenticationException("Invalid username or password entered.");
  1225. }
  1226. if (!string.IsNullOrEmpty(request.DeviceId)
  1227. && !_deviceManager.CanAccessDevice(user, request.DeviceId))
  1228. {
  1229. throw new SecurityException("User is not allowed access from this device.");
  1230. }
  1231. int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
  1232. int maxActiveSessions = user.MaxActiveSessions;
  1233. _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions);
  1234. if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
  1235. {
  1236. throw new SecurityException("User is at their maximum number of sessions.");
  1237. }
  1238. var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false);
  1239. var session = await LogSessionActivity(
  1240. request.App,
  1241. request.AppVersion,
  1242. request.DeviceId,
  1243. request.DeviceName,
  1244. request.RemoteEndPoint,
  1245. user).ConfigureAwait(false);
  1246. var returnResult = new AuthenticationResult
  1247. {
  1248. User = _userManager.GetUserDto(user, request.RemoteEndPoint),
  1249. SessionInfo = session,
  1250. AccessToken = token,
  1251. ServerId = _appHost.SystemId
  1252. };
  1253. AuthenticationSucceeded?.Invoke(this, new GenericEventArgs<AuthenticationResult>(returnResult));
  1254. return returnResult;
  1255. }
  1256. private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
  1257. {
  1258. var existing = (await _deviceManager.GetDevices(
  1259. new DeviceQuery
  1260. {
  1261. DeviceId = deviceId,
  1262. UserId = user.Id,
  1263. Limit = 1
  1264. }).ConfigureAwait(false)).Items.FirstOrDefault();
  1265. var allExistingForDevice = (await _deviceManager.GetDevices(
  1266. new DeviceQuery
  1267. {
  1268. DeviceId = deviceId
  1269. }).ConfigureAwait(false)).Items;
  1270. foreach (var auth in allExistingForDevice)
  1271. {
  1272. if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
  1273. {
  1274. try
  1275. {
  1276. await Logout(auth).ConfigureAwait(false);
  1277. }
  1278. catch (Exception ex)
  1279. {
  1280. _logger.LogError(ex, "Error while logging out.");
  1281. }
  1282. }
  1283. }
  1284. if (existing != null)
  1285. {
  1286. _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
  1287. return existing.AccessToken;
  1288. }
  1289. _logger.LogInformation("Creating new access token for user {0}", user.Id);
  1290. var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false);
  1291. return device.AccessToken;
  1292. }
  1293. /// <inheritdoc />
  1294. public async Task Logout(string accessToken)
  1295. {
  1296. CheckDisposed();
  1297. if (string.IsNullOrEmpty(accessToken))
  1298. {
  1299. throw new ArgumentNullException(nameof(accessToken));
  1300. }
  1301. var existing = (await _deviceManager.GetDevices(
  1302. new DeviceQuery
  1303. {
  1304. Limit = 1,
  1305. AccessToken = accessToken
  1306. }).ConfigureAwait(false)).Items;
  1307. if (existing.Count > 0)
  1308. {
  1309. await Logout(existing[0]).ConfigureAwait(false);
  1310. }
  1311. }
  1312. /// <inheritdoc />
  1313. public async Task Logout(Device existing)
  1314. {
  1315. CheckDisposed();
  1316. _logger.LogInformation("Logging out access token {0}", existing.AccessToken);
  1317. await _deviceManager.DeleteDevice(existing).ConfigureAwait(false);
  1318. var sessions = Sessions
  1319. .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase))
  1320. .ToList();
  1321. foreach (var session in sessions)
  1322. {
  1323. try
  1324. {
  1325. ReportSessionEnded(session.Id);
  1326. }
  1327. catch (Exception ex)
  1328. {
  1329. _logger.LogError("Error reporting session ended", ex);
  1330. }
  1331. }
  1332. }
  1333. /// <inheritdoc />
  1334. public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
  1335. {
  1336. CheckDisposed();
  1337. var existing = await _deviceManager.GetDevices(new DeviceQuery
  1338. {
  1339. UserId = userId
  1340. }).ConfigureAwait(false);
  1341. foreach (var info in existing.Items)
  1342. {
  1343. if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
  1344. {
  1345. await Logout(info).ConfigureAwait(false);
  1346. }
  1347. }
  1348. }
  1349. /// <summary>
  1350. /// Reports the capabilities.
  1351. /// </summary>
  1352. /// <param name="sessionId">The session identifier.</param>
  1353. /// <param name="capabilities">The capabilities.</param>
  1354. public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
  1355. {
  1356. CheckDisposed();
  1357. var session = GetSession(sessionId);
  1358. ReportCapabilities(session, capabilities, true);
  1359. }
  1360. private void ReportCapabilities(
  1361. SessionInfo session,
  1362. ClientCapabilities capabilities,
  1363. bool saveCapabilities)
  1364. {
  1365. session.Capabilities = capabilities;
  1366. if (saveCapabilities)
  1367. {
  1368. CapabilitiesChanged?.Invoke(
  1369. this,
  1370. new SessionEventArgs
  1371. {
  1372. SessionInfo = session
  1373. });
  1374. _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
  1375. }
  1376. }
  1377. /// <summary>
  1378. /// Converts a BaseItem to a BaseItemInfo.
  1379. /// </summary>
  1380. private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
  1381. {
  1382. if (item == null)
  1383. {
  1384. throw new ArgumentNullException(nameof(item));
  1385. }
  1386. var dtoOptions = _itemInfoDtoOptions;
  1387. if (_itemInfoDtoOptions == null)
  1388. {
  1389. dtoOptions = new DtoOptions
  1390. {
  1391. AddProgramRecordingInfo = false
  1392. };
  1393. var fields = dtoOptions.Fields.ToList();
  1394. fields.Remove(ItemFields.BasicSyncInfo);
  1395. fields.Remove(ItemFields.CanDelete);
  1396. fields.Remove(ItemFields.CanDownload);
  1397. fields.Remove(ItemFields.ChildCount);
  1398. fields.Remove(ItemFields.CustomRating);
  1399. fields.Remove(ItemFields.DateLastMediaAdded);
  1400. fields.Remove(ItemFields.DateLastRefreshed);
  1401. fields.Remove(ItemFields.DateLastSaved);
  1402. fields.Remove(ItemFields.DisplayPreferencesId);
  1403. fields.Remove(ItemFields.Etag);
  1404. fields.Remove(ItemFields.InheritedParentalRatingValue);
  1405. fields.Remove(ItemFields.ItemCounts);
  1406. fields.Remove(ItemFields.MediaSourceCount);
  1407. fields.Remove(ItemFields.MediaStreams);
  1408. fields.Remove(ItemFields.MediaSources);
  1409. fields.Remove(ItemFields.People);
  1410. fields.Remove(ItemFields.PlayAccess);
  1411. fields.Remove(ItemFields.People);
  1412. fields.Remove(ItemFields.ProductionLocations);
  1413. fields.Remove(ItemFields.RecursiveItemCount);
  1414. fields.Remove(ItemFields.RemoteTrailers);
  1415. fields.Remove(ItemFields.SeasonUserData);
  1416. fields.Remove(ItemFields.Settings);
  1417. fields.Remove(ItemFields.SortName);
  1418. fields.Remove(ItemFields.Tags);
  1419. fields.Remove(ItemFields.ExtraIds);
  1420. dtoOptions.Fields = fields.ToArray();
  1421. _itemInfoDtoOptions = dtoOptions;
  1422. }
  1423. var info = _dtoService.GetBaseItemDto(item, dtoOptions);
  1424. if (mediaSource != null)
  1425. {
  1426. info.MediaStreams = mediaSource.MediaStreams.ToArray();
  1427. }
  1428. return info;
  1429. }
  1430. private string GetImageCacheTag(User user)
  1431. {
  1432. try
  1433. {
  1434. return _imageProcessor.GetImageCacheTag(user);
  1435. }
  1436. catch (Exception e)
  1437. {
  1438. _logger.LogError(e, "Error getting image information for profile image");
  1439. return null;
  1440. }
  1441. }
  1442. /// <inheritdoc />
  1443. public void ReportNowViewingItem(string sessionId, string itemId)
  1444. {
  1445. if (string.IsNullOrEmpty(itemId))
  1446. {
  1447. throw new ArgumentNullException(nameof(itemId));
  1448. }
  1449. var item = _libraryManager.GetItemById(new Guid(itemId));
  1450. var session = GetSession(sessionId);
  1451. session.NowViewingItem = GetItemInfo(item, null);
  1452. }
  1453. /// <inheritdoc />
  1454. public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
  1455. {
  1456. var session = Sessions.FirstOrDefault(i =>
  1457. string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
  1458. if (session != null)
  1459. {
  1460. session.TranscodingInfo = info;
  1461. }
  1462. }
  1463. /// <inheritdoc />
  1464. public void ClearTranscodingInfo(string deviceId)
  1465. {
  1466. ReportTranscodingInfo(deviceId, null);
  1467. }
  1468. /// <inheritdoc />
  1469. public SessionInfo GetSession(string deviceId, string client, string version)
  1470. {
  1471. return Sessions.FirstOrDefault(i =>
  1472. string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
  1473. && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
  1474. }
  1475. /// <inheritdoc />
  1476. public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion)
  1477. {
  1478. if (info == null)
  1479. {
  1480. throw new ArgumentNullException(nameof(info));
  1481. }
  1482. var user = info.UserId == Guid.Empty
  1483. ? null
  1484. : _userManager.GetUserById(info.UserId);
  1485. appVersion = string.IsNullOrEmpty(appVersion)
  1486. ? info.AppVersion
  1487. : appVersion;
  1488. var deviceName = info.DeviceName;
  1489. var appName = info.AppName;
  1490. if (string.IsNullOrEmpty(deviceId))
  1491. {
  1492. deviceId = info.DeviceId;
  1493. }
  1494. // Prevent argument exception
  1495. if (string.IsNullOrEmpty(appVersion))
  1496. {
  1497. appVersion = "1";
  1498. }
  1499. return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
  1500. }
  1501. /// <inheritdoc />
  1502. public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
  1503. {
  1504. var items = (await _deviceManager.GetDevices(new DeviceQuery
  1505. {
  1506. AccessToken = token,
  1507. Limit = 1
  1508. }).ConfigureAwait(false)).Items;
  1509. if (items.Count == 0)
  1510. {
  1511. return null;
  1512. }
  1513. return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false);
  1514. }
  1515. /// <inheritdoc />
  1516. public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
  1517. {
  1518. CheckDisposed();
  1519. var adminUserIds = _userManager.Users
  1520. .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
  1521. .Select(i => i.Id)
  1522. .ToList();
  1523. return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
  1524. }
  1525. /// <inheritdoc />
  1526. public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken)
  1527. {
  1528. CheckDisposed();
  1529. var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
  1530. if (sessions.Count == 0)
  1531. {
  1532. return Task.CompletedTask;
  1533. }
  1534. return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
  1535. }
  1536. /// <inheritdoc />
  1537. public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
  1538. {
  1539. CheckDisposed();
  1540. var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
  1541. return SendMessageToSessions(sessions, name, data, cancellationToken);
  1542. }
  1543. /// <inheritdoc />
  1544. public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken)
  1545. {
  1546. CheckDisposed();
  1547. var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
  1548. return SendMessageToSessions(sessions, name, data, cancellationToken);
  1549. }
  1550. }
  1551. }