SessionManager.cs 63 KB

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