SessionManager.cs 76 KB

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