SessionManager.cs 65 KB

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