2
0
Эх сурвалжийг харах

Merge pull request #10976 from barronpm/ihostedservice

Replace IServerEntryPoint with IHostedService
Cody Robibero 1 жил өмнө
parent
commit
5ad7e4b749

+ 2 - 31
Emby.Server.Implementations/ApplicationHost.cs

@@ -62,7 +62,6 @@ using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.QuickConnect;
 using MediaBrowser.Controller.Resolvers;
@@ -393,7 +392,7 @@ namespace Emby.Server.Implementations
         /// Runs the startup tasks.
         /// </summary>
         /// <returns><see cref="Task" />.</returns>
-        public async Task RunStartupTasksAsync()
+        public Task RunStartupTasksAsync()
         {
             Logger.LogInformation("Running startup tasks");
 
@@ -405,38 +404,10 @@ namespace Emby.Server.Implementations
             Resolve<IMediaEncoder>().SetFFmpegPath();
 
             Logger.LogInformation("ServerId: {ServerId}", SystemId);
-
-            var entryPoints = GetExports<IServerEntryPoint>();
-
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
-
-            await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
-            Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
-
             Logger.LogInformation("Core startup complete");
             CoreStartupHasCompleted = true;
 
-            stopWatch.Restart();
-
-            await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
-            Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
-            stopWatch.Stop();
-        }
-
-        private IEnumerable<Task> StartEntryPoints(IEnumerable<IServerEntryPoint> entryPoints, bool isBeforeStartup)
-        {
-            foreach (var entryPoint in entryPoints)
-            {
-                if (isBeforeStartup != (entryPoint is IRunBeforeStartup))
-                {
-                    continue;
-                }
-
-                Logger.LogDebug("Starting entry point {Type}", entryPoint.GetType());
-
-                yield return entryPoint.RunAsync();
-            }
+            return Task.CompletedTask;
         }
 
         /// <inheritdoc/>

+ 23 - 22
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -13,19 +13,19 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints;
 
 /// <summary>
-/// A <see cref="IServerEntryPoint"/> that notifies users when libraries are updated.
+/// A <see cref="IHostedService"/> responsible for notifying users when libraries are updated.
 /// </summary>
-public sealed class LibraryChangedNotifier : IServerEntryPoint
+public sealed class LibraryChangedNotifier : IHostedService, IDisposable
 {
     private readonly ILibraryManager _libraryManager;
     private readonly IServerConfigurationManager _configurationManager;
@@ -70,7 +70,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
     }
 
     /// <inheritdoc />
-    public Task RunAsync()
+    public Task StartAsync(CancellationToken cancellationToken)
     {
         _libraryManager.ItemAdded += OnLibraryItemAdded;
         _libraryManager.ItemUpdated += OnLibraryItemUpdated;
@@ -83,6 +83,20 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
         return Task.CompletedTask;
     }
 
+    /// <inheritdoc />
+    public Task StopAsync(CancellationToken cancellationToken)
+    {
+        _libraryManager.ItemAdded -= OnLibraryItemAdded;
+        _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
+        _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
+
+        _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
+        _providerManager.RefreshStarted -= OnProviderRefreshStarted;
+        _providerManager.RefreshProgress -= OnProviderRefreshProgress;
+
+        return Task.CompletedTask;
+    }
+
     private void OnProviderRefreshProgress(object? sender, GenericEventArgs<Tuple<BaseItem, double>> e)
     {
         var item = e.Argument.Item1;
@@ -137,9 +151,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
     }
 
     private void OnProviderRefreshStarted(object? sender, GenericEventArgs<BaseItem> e)
-    {
-        OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
-    }
+        => OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
 
     private void OnProviderRefreshCompleted(object? sender, GenericEventArgs<BaseItem> e)
     {
@@ -342,7 +354,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
         return item.SourceType == SourceType.Library;
     }
 
-    private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
+    private static IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
     {
         var list = new List<string>();
 
@@ -363,7 +375,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
         return list.Distinct(StringComparer.Ordinal);
     }
 
-    private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
+    private T[] TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
         where T : BaseItem
     {
         // If the physical root changed, return the user root
@@ -384,18 +396,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
     /// <inheritdoc />
     public void Dispose()
     {
-        _libraryManager.ItemAdded -= OnLibraryItemAdded;
-        _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
-        _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
-
-        _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
-        _providerManager.RefreshStarted -= OnProviderRefreshStarted;
-        _providerManager.RefreshProgress -= OnProviderRefreshProgress;
-
-        if (_libraryUpdateTimer is not null)
-        {
-            _libraryUpdateTimer.Dispose();
-            _libraryUpdateTimer = null;
-        }
+        _libraryUpdateTimer?.Dispose();
+        _libraryUpdateTimer = null;
     }
 }

+ 46 - 42
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -8,14 +6,17 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Hosting;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
-    public sealed class UserDataChangeNotifier : IServerEntryPoint
+    /// <summary>
+    /// <see cref="IHostedService"/> responsible for notifying users when associated item data is updated.
+    /// </summary>
+    public sealed class UserDataChangeNotifier : IHostedService, IDisposable
     {
         private const int UpdateDuration = 500;
 
@@ -23,25 +24,43 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IUserDataManager _userDataManager;
         private readonly IUserManager _userManager;
 
-        private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
+        private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
+        private readonly object _syncLock = new();
 
-        private readonly object _syncLock = new object();
         private Timer? _updateTimer;
 
-        public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
+        /// </summary>
+        /// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
+        /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
+        /// <param name="userManager">The <see cref="IUserManager"/>.</param>
+        public UserDataChangeNotifier(
+            IUserDataManager userDataManager,
+            ISessionManager sessionManager,
+            IUserManager userManager)
         {
             _userDataManager = userDataManager;
             _sessionManager = sessionManager;
             _userManager = userManager;
         }
 
-        public Task RunAsync()
+        /// <inheritdoc />
+        public Task StartAsync(CancellationToken cancellationToken)
         {
             _userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
 
             return Task.CompletedTask;
         }
 
+        /// <inheritdoc />
+        public Task StopAsync(CancellationToken cancellationToken)
+        {
+            _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
+
+            return Task.CompletedTask;
+        }
+
         private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
         {
             if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
@@ -103,55 +122,40 @@ namespace Emby.Server.Implementations.EntryPoints
                 }
             }
 
-            await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
-        }
-
-        private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
-        {
-            foreach ((var key, var value) in changes)
+            foreach (var (userId, changedItems) in changes)
             {
-                await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
+                await _sessionManager.SendMessageToUserSessions(
+                    [userId],
+                    SessionMessageType.UserDataChanged,
+                    () => GetUserDataChangeInfo(userId, changedItems),
+                    default).ConfigureAwait(false);
             }
         }
 
-        private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
-        {
-            return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
-        }
-
         private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
         {
             var user = _userManager.GetUserById(userId);
 
-            var dtoList = changedItems
-                .DistinctBy(x => x.Id)
-                .Select(i =>
-                {
-                    var dto = _userDataManager.GetUserDataDto(i, user);
-                    dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
-                    return dto;
-                })
-                .ToArray();
-
-            var userIdString = userId.ToString("N", CultureInfo.InvariantCulture);
-
             return new UserDataChangeInfo
             {
-                UserId = userIdString,
-
-                UserDataList = dtoList
+                UserId = userId.ToString("N", CultureInfo.InvariantCulture),
+                UserDataList = changedItems
+                    .DistinctBy(x => x.Id)
+                    .Select(i =>
+                    {
+                        var dto = _userDataManager.GetUserDataDto(i, user);
+                        dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
+                        return dto;
+                    })
+                    .ToArray()
             };
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            if (_updateTimer is not null)
-            {
-                _updateTimer.Dispose();
-                _updateTimer = null;
-            }
-
-            _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
+            _updateTimer?.Dispose();
+            _updateTimer = null;
         }
     }
 }

+ 20 - 51
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -11,11 +9,13 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.IO
 {
-    public class LibraryMonitor : ILibraryMonitor
+    /// <inheritdoc cref="ILibraryMonitor" />
+    public sealed class LibraryMonitor : ILibraryMonitor, IDisposable
     {
         private readonly ILogger<LibraryMonitor> _logger;
         private readonly ILibraryManager _libraryManager;
@@ -25,19 +25,19 @@ namespace Emby.Server.Implementations.IO
         /// <summary>
         /// The file system watchers.
         /// </summary>
-        private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
+        private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.OrdinalIgnoreCase);
 
         /// <summary>
         /// The affected paths.
         /// </summary>
-        private readonly List<FileRefresher> _activeRefreshers = new List<FileRefresher>();
+        private readonly List<FileRefresher> _activeRefreshers = [];
 
         /// <summary>
         /// A dynamic list of paths that should be ignored.  Added to during our own file system modifications.
         /// </summary>
-        private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
 
-        private bool _disposed = false;
+        private bool _disposed;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
@@ -46,34 +46,31 @@ namespace Emby.Server.Implementations.IO
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="fileSystem">The filesystem.</param>
+        /// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
         public LibraryMonitor(
             ILogger<LibraryMonitor> logger,
             ILibraryManager libraryManager,
             IServerConfigurationManager configurationManager,
-            IFileSystem fileSystem)
+            IFileSystem fileSystem,
+            IHostApplicationLifetime appLifetime)
         {
             _libraryManager = libraryManager;
             _logger = logger;
             _configurationManager = configurationManager;
             _fileSystem = fileSystem;
-        }
 
-        /// <summary>
-        /// Add the path to our temporary ignore list.  Use when writing to a path within our listening scope.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        private void TemporarilyIgnore(string path)
-        {
-            _tempIgnoredPaths[path] = path;
+            appLifetime.ApplicationStarted.Register(Start);
         }
 
+        /// <inheritdoc />
         public void ReportFileSystemChangeBeginning(string path)
         {
             ArgumentException.ThrowIfNullOrEmpty(path);
 
-            TemporarilyIgnore(path);
+            _tempIgnoredPaths[path] = path;
         }
 
+        /// <inheritdoc />
         public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
         {
             ArgumentException.ThrowIfNullOrEmpty(path);
@@ -107,14 +104,10 @@ namespace Emby.Server.Implementations.IO
 
             var options = _libraryManager.GetLibraryOptions(item);
 
-            if (options is not null)
-            {
-                return options.EnableRealtimeMonitor;
-            }
-
-            return false;
+            return options is not null && options.EnableRealtimeMonitor;
         }
 
+        /// <inheritdoc />
         public void Start()
         {
             _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
@@ -306,20 +299,11 @@ namespace Emby.Server.Implementations.IO
             {
                 if (removeFromList)
                 {
-                    RemoveWatcherFromList(watcher);
+                    _fileSystemWatchers.TryRemove(watcher.Path, out _);
                 }
             }
         }
 
-        /// <summary>
-        /// Removes the watcher from list.
-        /// </summary>
-        /// <param name="watcher">The watcher.</param>
-        private void RemoveWatcherFromList(FileSystemWatcher watcher)
-        {
-            _fileSystemWatchers.TryRemove(watcher.Path, out _);
-        }
-
         /// <summary>
         /// Handles the Error event of the watcher control.
         /// </summary>
@@ -352,6 +336,7 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
+        /// <inheritdoc />
         public void ReportFileSystemChanged(string path)
         {
             ArgumentException.ThrowIfNullOrEmpty(path);
@@ -479,31 +464,15 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        /// <summary>
-        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-        /// </summary>
+        /// <inheritdoc />
         public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool disposing)
         {
             if (_disposed)
             {
                 return;
             }
 
-            if (disposing)
-            {
-                Stop();
-            }
-
+            Stop();
             _disposed = true;
         }
     }

+ 0 - 35
Emby.Server.Implementations/IO/LibraryMonitorStartup.cs

@@ -1,35 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-
-namespace Emby.Server.Implementations.IO
-{
-    /// <summary>
-    /// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
-    /// </summary>
-    public sealed class LibraryMonitorStartup : IServerEntryPoint
-    {
-        private readonly ILibraryMonitor _monitor;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
-        /// </summary>
-        /// <param name="monitor">The library monitor.</param>
-        public LibraryMonitorStartup(ILibraryMonitor monitor)
-        {
-            _monitor = monitor;
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            _monitor.Start();
-            return Task.CompletedTask;
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-        }
-    }
-}

+ 0 - 64
Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs

@@ -1,64 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Queries;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-
-namespace Jellyfin.Server.Implementations.Users
-{
-    public sealed class DeviceAccessEntryPoint : IServerEntryPoint
-    {
-        private readonly IUserManager _userManager;
-        private readonly IDeviceManager _deviceManager;
-        private readonly ISessionManager _sessionManager;
-
-        public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
-        {
-            _userManager = userManager;
-            _deviceManager = deviceManager;
-            _sessionManager = sessionManager;
-        }
-
-        public Task RunAsync()
-        {
-            _userManager.OnUserUpdated += OnUserUpdated;
-
-            return Task.CompletedTask;
-        }
-
-        public void Dispose()
-        {
-        }
-
-        private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
-        {
-            var user = e.Argument;
-            if (!user.HasPermission(PermissionKind.EnableAllDevices))
-            {
-                await UpdateDeviceAccess(user).ConfigureAwait(false);
-            }
-        }
-
-        private async Task UpdateDeviceAccess(User user)
-        {
-            var existing = (await _deviceManager.GetDevices(new DeviceQuery
-            {
-                UserId = user.Id
-            }).ConfigureAwait(false)).Items;
-
-            foreach (var device in existing)
-            {
-                if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
-                {
-                    await _sessionManager.Logout(device).ConfigureAwait(false);
-                }
-            }
-        }
-    }
-}

+ 76 - 0
Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs

@@ -0,0 +1,76 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using Microsoft.Extensions.Hosting;
+
+namespace Jellyfin.Server.Implementations.Users;
+
+/// <summary>
+/// <see cref="IHostedService"/> responsible for managing user device permissions.
+/// </summary>
+public sealed class DeviceAccessHost : IHostedService
+{
+    private readonly IUserManager _userManager;
+    private readonly IDeviceManager _deviceManager;
+    private readonly ISessionManager _sessionManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="DeviceAccessHost"/> class.
+    /// </summary>
+    /// <param name="userManager">The <see cref="IUserManager"/>.</param>
+    /// <param name="deviceManager">The <see cref="IDeviceManager"/>.</param>
+    /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
+    public DeviceAccessHost(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
+    {
+        _userManager = userManager;
+        _deviceManager = deviceManager;
+        _sessionManager = sessionManager;
+    }
+
+    /// <inheritdoc />
+    public Task StartAsync(CancellationToken cancellationToken)
+    {
+        _userManager.OnUserUpdated += OnUserUpdated;
+
+        return Task.CompletedTask;
+    }
+
+    /// <inheritdoc />
+    public Task StopAsync(CancellationToken cancellationToken)
+    {
+        _userManager.OnUserUpdated -= OnUserUpdated;
+
+        return Task.CompletedTask;
+    }
+
+    private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
+    {
+        var user = e.Argument;
+        if (!user.HasPermission(PermissionKind.EnableAllDevices))
+        {
+            await UpdateDeviceAccess(user).ConfigureAwait(false);
+        }
+    }
+
+    private async Task UpdateDeviceAccess(User user)
+    {
+        var existing = (await _deviceManager.GetDevices(new DeviceQuery
+        {
+            UserId = user.Id
+        }).ConfigureAwait(false)).Items;
+
+        foreach (var device in existing)
+        {
+            if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
+            {
+                await _sessionManager.Logout(device).ConfigureAwait(false);
+            }
+        }
+    }
+}

+ 10 - 0
Jellyfin.Server/Startup.cs

@@ -4,7 +4,10 @@ using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Text;
+using Emby.Server.Implementations.EntryPoints;
 using Jellyfin.Api.Middleware;
+using Jellyfin.LiveTv;
+using Jellyfin.LiveTv.EmbyTV;
 using Jellyfin.LiveTv.Extensions;
 using Jellyfin.MediaEncoding.Hls.Extensions;
 using Jellyfin.Networking;
@@ -17,6 +20,7 @@ using Jellyfin.Server.Infrastructure;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Extensions;
+using MediaBrowser.XbmcMetadata;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Mvc;
@@ -124,7 +128,13 @@ namespace Jellyfin.Server
             services.AddHlsPlaylistGenerator();
             services.AddLiveTvServices();
 
+            services.AddHostedService<LiveTvHost>();
             services.AddHostedService<AutoDiscoveryHost>();
+            services.AddHostedService<PortForwardingHost>();
+            services.AddHostedService<NfoUserDataSaver>();
+            services.AddHostedService<LibraryChangedNotifier>();
+            services.AddHostedService<UserDataChangeNotifier>();
+            services.AddHostedService<RecordingNotifier>();
         }
 
         /// <summary>

+ 4 - 5
MediaBrowser.Controller/Library/ILibraryMonitor.cs

@@ -1,10 +1,9 @@
-#pragma warning disable CS1591
-
-using System;
-
 namespace MediaBrowser.Controller.Library
 {
-    public interface ILibraryMonitor : IDisposable
+    /// <summary>
+    /// Service responsible for monitoring library filesystems for changes.
+    /// </summary>
+    public interface ILibraryMonitor
     {
         /// <summary>
         /// Starts this instance.

+ 0 - 9
MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs

@@ -1,9 +0,0 @@
-namespace MediaBrowser.Controller.Plugins
-{
-    /// <summary>
-    /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task.
-    /// </summary>
-    public interface IRunBeforeStartup
-    {
-    }
-}

+ 0 - 20
MediaBrowser.Controller/Plugins/IServerEntryPoint.cs

@@ -1,20 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Plugins
-{
-    /// <summary>
-    /// Represents an entry point for a module in the application. This interface is scanned for automatically and
-    /// provides a hook to initialize the module at application start.
-    /// The entry point can additionally be flagged as a pre-startup task by implementing the
-    /// <see cref="IRunBeforeStartup"/> interface.
-    /// </summary>
-    public interface IServerEntryPoint : IDisposable
-    {
-        /// <summary>
-        /// Run the initialization for this module. This method is invoked at application start.
-        /// </summary>
-        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
-        Task RunAsync();
-    }
-}

+ 0 - 78
MediaBrowser.XbmcMetadata/EntryPoint.cs

@@ -1,78 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.XbmcMetadata.Configuration;
-using MediaBrowser.XbmcMetadata.Savers;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.XbmcMetadata
-{
-    public sealed class EntryPoint : IServerEntryPoint
-    {
-        private readonly IUserDataManager _userDataManager;
-        private readonly ILogger<EntryPoint> _logger;
-        private readonly IProviderManager _providerManager;
-        private readonly IConfigurationManager _config;
-
-        public EntryPoint(
-            IUserDataManager userDataManager,
-            ILogger<EntryPoint> logger,
-            IProviderManager providerManager,
-            IConfigurationManager config)
-        {
-            _userDataManager = userDataManager;
-            _logger = logger;
-            _providerManager = providerManager;
-            _config = config;
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            _userDataManager.UserDataSaved += OnUserDataSaved;
-
-            return Task.CompletedTask;
-        }
-
-        private void OnUserDataSaved(object? sender, UserDataSaveEventArgs e)
-        {
-            if (e.SaveReason == UserDataSaveReason.PlaybackFinished || e.SaveReason == UserDataSaveReason.TogglePlayed || e.SaveReason == UserDataSaveReason.UpdateUserRating)
-            {
-                if (!string.IsNullOrWhiteSpace(_config.GetNfoConfiguration().UserId))
-                {
-                    _ = SaveMetadataForItemAsync(e.Item, ItemUpdateType.MetadataDownload);
-                }
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            _userDataManager.UserDataSaved -= OnUserDataSaved;
-        }
-
-        private async Task SaveMetadataForItemAsync(BaseItem item, ItemUpdateType updateReason)
-        {
-            if (!item.IsFileProtocol || !item.SupportsLocalMetadata)
-            {
-                return;
-            }
-
-            try
-            {
-                await _providerManager.SaveMetadataAsync(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name);
-            }
-        }
-    }
-}

+ 87 - 0
MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs

@@ -0,0 +1,87 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.XbmcMetadata.Configuration;
+using MediaBrowser.XbmcMetadata.Savers;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.XbmcMetadata;
+
+/// <summary>
+/// <see cref="IHostedService"/> responsible for updating NFO files' user data.
+/// </summary>
+public sealed class NfoUserDataSaver : IHostedService
+{
+    private readonly ILogger<NfoUserDataSaver> _logger;
+    private readonly IConfigurationManager _config;
+    private readonly IUserDataManager _userDataManager;
+    private readonly IProviderManager _providerManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="NfoUserDataSaver"/> class.
+    /// </summary>
+    /// <param name="logger">The <see cref="ILogger"/>.</param>
+    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+    /// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
+    /// <param name="providerManager">The <see cref="IProviderManager"/>.</param>
+    public NfoUserDataSaver(
+        ILogger<NfoUserDataSaver> logger,
+        IConfigurationManager config,
+        IUserDataManager userDataManager,
+        IProviderManager providerManager)
+    {
+        _logger = logger;
+        _config = config;
+        _userDataManager = userDataManager;
+        _providerManager = providerManager;
+    }
+
+    /// <inheritdoc />
+    public Task StartAsync(CancellationToken cancellationToken)
+    {
+        _userDataManager.UserDataSaved += OnUserDataSaved;
+        return Task.CompletedTask;
+    }
+
+    /// <inheritdoc />
+    public Task StopAsync(CancellationToken cancellationToken)
+    {
+        _userDataManager.UserDataSaved -= OnUserDataSaved;
+        return Task.CompletedTask;
+    }
+
+    private async void OnUserDataSaved(object? sender, UserDataSaveEventArgs e)
+    {
+        if (e.SaveReason is not (UserDataSaveReason.PlaybackFinished
+            or UserDataSaveReason.TogglePlayed or UserDataSaveReason.UpdateUserRating))
+        {
+            return;
+        }
+
+        if (string.IsNullOrWhiteSpace(_config.GetNfoConfiguration().UserId))
+        {
+            return;
+        }
+
+        var item = e.Item;
+        if (!item.IsFileProtocol || !item.SupportsLocalMetadata)
+        {
+            return;
+        }
+
+        try
+        {
+            await _providerManager.SaveMetadataAsync(item, ItemUpdateType.MetadataDownload, [BaseNfoSaver.SaverName])
+                .ConfigureAwait(false);
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name);
+        }
+    }
+}

+ 0 - 21
src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs

@@ -1,21 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Plugins;
-
-namespace Jellyfin.LiveTv.EmbyTV
-{
-    public sealed class EntryPoint : IServerEntryPoint
-    {
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            return EmbyTV.Current.Start();
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-        }
-    }
-}

+ 31 - 0
src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs

@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.LiveTv;
+using Microsoft.Extensions.Hosting;
+
+namespace Jellyfin.LiveTv.EmbyTV;
+
+/// <summary>
+/// <see cref="IHostedService"/> responsible for initializing Live TV.
+/// </summary>
+public sealed class LiveTvHost : IHostedService
+{
+    private readonly EmbyTV _service;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="LiveTvHost"/> class.
+    /// </summary>
+    /// <param name="services">The available <see cref="ILiveTvService"/>s.</param>
+    public LiveTvHost(IEnumerable<ILiveTvService> services)
+    {
+        _service = services.OfType<EmbyTV>().First();
+    }
+
+    /// <inheritdoc />
+    public Task StartAsync(CancellationToken cancellationToken) => _service.Start();
+
+    /// <inheritdoc />
+    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}

+ 38 - 35
src/Jellyfin.LiveTv/RecordingNotifier.cs

@@ -1,7 +1,3 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
 using System;
 using System.Linq;
 using System.Threading;
@@ -10,34 +6,44 @@ using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.LiveTv
 {
-    public sealed class RecordingNotifier : IServerEntryPoint
+    /// <summary>
+    /// <see cref="IHostedService"/> responsible for notifying users when a LiveTV recording is completed.
+    /// </summary>
+    public sealed class RecordingNotifier : IHostedService
     {
-        private readonly ILiveTvManager _liveTvManager;
+        private readonly ILogger<RecordingNotifier> _logger;
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
-        private readonly ILogger<RecordingNotifier> _logger;
+        private readonly ILiveTvManager _liveTvManager;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RecordingNotifier"/> class.
+        /// </summary>
+        /// <param name="logger">The <see cref="ILogger"/>.</param>
+        /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
+        /// <param name="userManager">The <see cref="IUserManager"/>.</param>
+        /// <param name="liveTvManager">The <see cref="ILiveTvManager"/>.</param>
         public RecordingNotifier(
+            ILogger<RecordingNotifier> logger,
             ISessionManager sessionManager,
             IUserManager userManager,
-            ILogger<RecordingNotifier> logger,
             ILiveTvManager liveTvManager)
         {
+            _logger = logger;
             _sessionManager = sessionManager;
             _userManager = userManager;
-            _logger = logger;
             _liveTvManager = liveTvManager;
         }
 
         /// <inheritdoc />
-        public Task RunAsync()
+        public Task StartAsync(CancellationToken cancellationToken)
         {
             _liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled;
             _liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled;
@@ -47,29 +53,35 @@ namespace Jellyfin.LiveTv
             return Task.CompletedTask;
         }
 
-        private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
+        /// <inheritdoc />
+        public Task StopAsync(CancellationToken cancellationToken)
         {
-            await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
-        }
+            _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled;
+            _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled;
+            _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated;
+            _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated;
 
-        private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
-        {
-            await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
+            return Task.CompletedTask;
         }
 
-        private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
-        {
-            await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
-        }
+        private async void OnLiveTvManagerSeriesTimerCreated(object? sender, GenericEventArgs<TimerEventInfo> e)
+            => await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
 
-        private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
-        {
-            await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
-        }
+        private async void OnLiveTvManagerTimerCreated(object? sender, GenericEventArgs<TimerEventInfo> e)
+            => await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
+
+        private async void OnLiveTvManagerSeriesTimerCancelled(object? sender, GenericEventArgs<TimerEventInfo> e)
+            => await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
+
+        private async void OnLiveTvManagerTimerCancelled(object? sender, GenericEventArgs<TimerEventInfo> e)
+            => await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
 
         private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
         {
-            var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
+            var users = _userManager.Users
+                .Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess))
+                .Select(i => i.Id)
+                .ToList();
 
             try
             {
@@ -80,14 +92,5 @@ namespace Jellyfin.LiveTv
                 _logger.LogError(ex, "Error sending message");
             }
         }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled;
-            _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled;
-            _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated;
-            _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated;
-        }
     }
 }

+ 34 - 37
src/Jellyfin.Networking/ExternalPortForwarding.cs → src/Jellyfin.Networking/PortForwardingHost.cs

@@ -1,7 +1,3 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -12,36 +8,34 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Mono.Nat;
 
 namespace Jellyfin.Networking;
 
 /// <summary>
-/// Server entrypoint handling external port forwarding.
+/// <see cref="IHostedService"/> responsible for UPnP port forwarding.
 /// </summary>
-public sealed class ExternalPortForwarding : IServerEntryPoint
+public sealed class PortForwardingHost : IHostedService, IDisposable
 {
     private readonly IServerApplicationHost _appHost;
-    private readonly ILogger<ExternalPortForwarding> _logger;
+    private readonly ILogger<PortForwardingHost> _logger;
     private readonly IServerConfigurationManager _config;
+    private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new();
 
-    private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
-
-    private Timer _timer;
-    private string _configIdentifier;
-
+    private Timer? _timer;
+    private string? _configIdentifier;
     private bool _disposed;
 
     /// <summary>
-    /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class.
+    /// Initializes a new instance of the <see cref="PortForwardingHost"/> class.
     /// </summary>
     /// <param name="logger">The logger.</param>
     /// <param name="appHost">The application host.</param>
     /// <param name="config">The configuration manager.</param>
-    public ExternalPortForwarding(
-        ILogger<ExternalPortForwarding> logger,
+    public PortForwardingHost(
+        ILogger<PortForwardingHost> logger,
         IServerApplicationHost appHost,
         IServerConfigurationManager config)
     {
@@ -66,7 +60,7 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
             .ToString();
     }
 
-    private void OnConfigurationUpdated(object sender, EventArgs e)
+    private void OnConfigurationUpdated(object? sender, EventArgs e)
     {
         var oldConfigIdentifier = _configIdentifier;
         _configIdentifier = GetConfigIdentifier();
@@ -79,7 +73,7 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
     }
 
     /// <inheritdoc />
-    public Task RunAsync()
+    public Task StartAsync(CancellationToken cancellationToken)
     {
         Start();
 
@@ -88,6 +82,14 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
         return Task.CompletedTask;
     }
 
+    /// <inheritdoc />
+    public Task StopAsync(CancellationToken cancellationToken)
+    {
+        Stop();
+
+        return Task.CompletedTask;
+    }
+
     private void Start()
     {
         var config = _config.GetNetworkConfiguration();
@@ -101,7 +103,8 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
         NatUtility.DeviceFound += OnNatUtilityDeviceFound;
         NatUtility.StartDiscovery();
 
-        _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
+        _timer?.Dispose();
+        _timer = new Timer(_ => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
     }
 
     private void Stop()
@@ -112,13 +115,23 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
         NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
 
         _timer?.Dispose();
+        _timer = null;
     }
 
-    private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
+    private async void OnNatUtilityDeviceFound(object? sender, DeviceEventArgs e)
     {
+        ObjectDisposedException.ThrowIf(_disposed, this);
+
         try
         {
-            await CreateRules(e.Device).ConfigureAwait(false);
+            // On some systems the device discovered event seems to fire repeatedly
+            // This check will help ensure we're not trying to port map the same device over and over
+            if (!_createdRules.TryAdd(e.Device.DeviceEndpoint, 0))
+            {
+                return;
+            }
+
+            await Task.WhenAll(CreatePortMaps(e.Device)).ConfigureAwait(false);
         }
         catch (Exception ex)
         {
@@ -126,20 +139,6 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
         }
     }
 
-    private Task CreateRules(INatDevice device)
-    {
-        ObjectDisposedException.ThrowIf(_disposed, this);
-
-        // On some systems the device discovered event seems to fire repeatedly
-        // This check will help ensure we're not trying to port map the same device over and over
-        if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
-        {
-            return Task.CompletedTask;
-        }
-
-        return Task.WhenAll(CreatePortMaps(device));
-    }
-
     private IEnumerable<Task> CreatePortMaps(INatDevice device)
     {
         var config = _config.GetNetworkConfiguration();
@@ -185,8 +184,6 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
 
         _config.ConfigurationUpdated -= OnConfigurationUpdated;
 
-        Stop();
-
         _timer?.Dispose();
         _timer = null;