Patrick Barron 1 год назад
Родитель
Сommit
9c2c066e6f

+ 1 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -695,7 +695,7 @@ namespace Emby.Server.Implementations
                 GetExports<IMetadataSaver>(),
                 GetExports<IMetadataSaver>(),
                 GetExports<IExternalId>());
                 GetExports<IExternalId>());
 
 
-            Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
+            Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<IListingsProvider>());
 
 
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
         }
         }

+ 8 - 11
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -10,7 +10,6 @@ using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
@@ -43,6 +42,7 @@ namespace Jellyfin.Api.Controllers;
 public class LiveTvController : BaseJellyfinApiController
 public class LiveTvController : BaseJellyfinApiController
 {
 {
     private readonly ILiveTvManager _liveTvManager;
     private readonly ILiveTvManager _liveTvManager;
+    private readonly ITunerHostManager _tunerHostManager;
     private readonly IUserManager _userManager;
     private readonly IUserManager _userManager;
     private readonly IHttpClientFactory _httpClientFactory;
     private readonly IHttpClientFactory _httpClientFactory;
     private readonly ILibraryManager _libraryManager;
     private readonly ILibraryManager _libraryManager;
@@ -55,6 +55,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// Initializes a new instance of the <see cref="LiveTvController"/> class.
     /// Initializes a new instance of the <see cref="LiveTvController"/> class.
     /// </summary>
     /// </summary>
     /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
     /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
+    /// <param name="tunerHostManager">Instance of the <see cref="ITunerHostManager"/> interface.</param>
     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
     /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
     /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
@@ -64,6 +65,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
     /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
     public LiveTvController(
     public LiveTvController(
         ILiveTvManager liveTvManager,
         ILiveTvManager liveTvManager,
+        ITunerHostManager tunerHostManager,
         IUserManager userManager,
         IUserManager userManager,
         IHttpClientFactory httpClientFactory,
         IHttpClientFactory httpClientFactory,
         ILibraryManager libraryManager,
         ILibraryManager libraryManager,
@@ -73,6 +75,7 @@ public class LiveTvController : BaseJellyfinApiController
         ITranscodeManager transcodeManager)
         ITranscodeManager transcodeManager)
     {
     {
         _liveTvManager = liveTvManager;
         _liveTvManager = liveTvManager;
+        _tunerHostManager = tunerHostManager;
         _userManager = userManager;
         _userManager = userManager;
         _httpClientFactory = httpClientFactory;
         _httpClientFactory = httpClientFactory;
         _libraryManager = libraryManager;
         _libraryManager = libraryManager;
@@ -951,9 +954,7 @@ public class LiveTvController : BaseJellyfinApiController
     [Authorize(Policy = Policies.LiveTvManagement)]
     [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
     public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
-    {
-        return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
-    }
+        => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
 
 
     /// <summary>
     /// <summary>
     /// Deletes a tuner host.
     /// Deletes a tuner host.
@@ -1130,10 +1131,8 @@ public class LiveTvController : BaseJellyfinApiController
     [HttpGet("TunerHosts/Types")]
     [HttpGet("TunerHosts/Types")]
     [Authorize(Policy = Policies.LiveTvAccess)]
     [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
-    {
-        return _liveTvManager.GetTunerHostTypes();
-    }
+    public IEnumerable<NameIdPair> GetTunerHostTypes()
+        => _tunerHostManager.GetTunerHostTypes();
 
 
     /// <summary>
     /// <summary>
     /// Discover tuners.
     /// Discover tuners.
@@ -1146,9 +1145,7 @@ public class LiveTvController : BaseJellyfinApiController
     [Authorize(Policy = Policies.LiveTvManagement)]
     [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
     public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
-    {
-        return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
-    }
+        => await _tunerHostManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
 
 
     /// <summary>
     /// <summary>
     /// Gets a live tv recording stream.
     /// Gets a live tv recording stream.

+ 1 - 14
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -71,9 +71,8 @@ namespace MediaBrowser.Controller.LiveTv
         /// Adds the parts.
         /// Adds the parts.
         /// </summary>
         /// </summary>
         /// <param name="services">The services.</param>
         /// <param name="services">The services.</param>
-        /// <param name="tunerHosts">The tuner hosts.</param>
         /// <param name="listingProviders">The listing providers.</param>
         /// <param name="listingProviders">The listing providers.</param>
-        void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders);
+        void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders);
 
 
         /// <summary>
         /// <summary>
         /// Gets the timer.
         /// Gets the timer.
@@ -253,14 +252,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
         Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
 
 
-        /// <summary>
-        /// Saves the tuner host.
-        /// </summary>
-        /// <param name="info">Turner host to save.</param>
-        /// <param name="dataSourceChanged">Option to specify that data source has changed.</param>
-        /// <returns>Tuner host information wrapped in a task.</returns>
-        Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
-
         /// <summary>
         /// <summary>
         /// Saves the listing provider.
         /// Saves the listing provider.
         /// </summary>
         /// </summary>
@@ -298,10 +289,6 @@ namespace MediaBrowser.Controller.LiveTv
 
 
         Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
         Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
 
 
-        List<NameIdPair> GetTunerHostTypes();
-
-        Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
-
         string GetEmbyTvActiveRecordingPath(string id);
         string GetEmbyTvActiveRecordingPath(string id);
 
 
         ActiveRecordingInfo GetActiveRecordingInfo(string path);
         ActiveRecordingInfo GetActiveRecordingInfo(string path);

+ 47 - 0
MediaBrowser.Controller/LiveTv/ITunerHostManager.cs

@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.LiveTv;
+
+/// <summary>
+/// Service responsible for managing the <see cref="ITunerHost"/>s.
+/// </summary>
+public interface ITunerHostManager
+{
+    /// <summary>
+    /// Gets the available <see cref="ITunerHost"/>s.
+    /// </summary>
+    IReadOnlyList<ITunerHost> TunerHosts { get; }
+
+    /// <summary>
+    /// Gets the <see cref="NameIdPair"/>s for the available <see cref="ITunerHost"/>s.
+    /// </summary>
+    /// <returns>The <see cref="NameIdPair"/>s.</returns>
+    IEnumerable<NameIdPair> GetTunerHostTypes();
+
+    /// <summary>
+    /// Saves the tuner host.
+    /// </summary>
+    /// <param name="info">Turner host to save.</param>
+    /// <param name="dataSourceChanged">Option to specify that data source has changed.</param>
+    /// <returns>Tuner host information wrapped in a task.</returns>
+    Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
+
+    /// <summary>
+    /// Discovers the available tuners.
+    /// </summary>
+    /// <param name="newDevicesOnly">A value indicating whether to only return new devices.</param>
+    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
+    /// <returns>The <see cref="TunerHostInfo"/>s.</returns>
+    Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
+
+    /// <summary>
+    /// Scans for tuner devices that have changed URLs.
+    /// </summary>
+    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
+    /// <returns>A task that represents the scanning operation.</returns>
+    Task ScanForTunerDeviceChanges(CancellationToken cancellationToken);
+}

+ 7 - 82
src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs

@@ -44,8 +44,6 @@ namespace Jellyfin.LiveTv.EmbyTV
     {
     {
         public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
         public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
 
 
-        private const int TunerDiscoveryDurationMs = 3000;
-
         private readonly ILogger<EmbyTV> _logger;
         private readonly ILogger<EmbyTV> _logger;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
@@ -54,6 +52,7 @@ namespace Jellyfin.LiveTv.EmbyTV
         private readonly TimerManager _timerProvider;
         private readonly TimerManager _timerProvider;
 
 
         private readonly LiveTvManager _liveTvManager;
         private readonly LiveTvManager _liveTvManager;
+        private readonly ITunerHostManager _tunerHostManager;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
 
 
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly ILibraryMonitor _libraryMonitor;
@@ -80,6 +79,7 @@ namespace Jellyfin.LiveTv.EmbyTV
             IHttpClientFactory httpClientFactory,
             IHttpClientFactory httpClientFactory,
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             ILiveTvManager liveTvManager,
             ILiveTvManager liveTvManager,
+            ITunerHostManager tunerHostManager,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             ILibraryMonitor libraryMonitor,
             ILibraryMonitor libraryMonitor,
@@ -97,6 +97,7 @@ namespace Jellyfin.LiveTv.EmbyTV
             _providerManager = providerManager;
             _providerManager = providerManager;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _liveTvManager = (LiveTvManager)liveTvManager;
             _liveTvManager = (LiveTvManager)liveTvManager;
+            _tunerHostManager = tunerHostManager;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _streamHelper = streamHelper;
             _streamHelper = streamHelper;
 
 
@@ -310,7 +311,7 @@ namespace Jellyfin.LiveTv.EmbyTV
         {
         {
             var list = new List<ChannelInfo>();
             var list = new List<ChannelInfo>();
 
 
-            foreach (var hostInstance in _liveTvManager.TunerHosts)
+            foreach (var hostInstance in _tunerHostManager.TunerHosts)
             {
             {
                 try
                 try
                 {
                 {
@@ -510,7 +511,7 @@ namespace Jellyfin.LiveTv.EmbyTV
         {
         {
             var list = new List<ChannelInfo>();
             var list = new List<ChannelInfo>();
 
 
-            foreach (var hostInstance in _liveTvManager.TunerHosts)
+            foreach (var hostInstance in _tunerHostManager.TunerHosts)
             {
             {
                 try
                 try
                 {
                 {
@@ -966,7 +967,7 @@ namespace Jellyfin.LiveTv.EmbyTV
                 return result;
                 return result;
             }
             }
 
 
-            foreach (var hostInstance in _liveTvManager.TunerHosts)
+            foreach (var hostInstance in _tunerHostManager.TunerHosts)
             {
             {
                 try
                 try
                 {
                 {
@@ -998,7 +999,7 @@ namespace Jellyfin.LiveTv.EmbyTV
                 throw new ArgumentNullException(nameof(channelId));
                 throw new ArgumentNullException(nameof(channelId));
             }
             }
 
 
-            foreach (var hostInstance in _liveTvManager.TunerHosts)
+            foreach (var hostInstance in _tunerHostManager.TunerHosts)
             {
             {
                 try
                 try
                 {
                 {
@@ -2537,81 +2538,5 @@ namespace Jellyfin.LiveTv.EmbyTV
                 };
                 };
             }
             }
         }
         }
-
-        public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
-        {
-            var list = new List<TunerHostInfo>();
-
-            var configuredDeviceIds = _config.GetLiveTvConfiguration().TunerHosts
-               .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
-               .Select(i => i.DeviceId)
-               .ToList();
-
-            foreach (var host in _liveTvManager.TunerHosts)
-            {
-                var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
-
-                if (newDevicesOnly)
-                {
-                    discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase))
-                            .ToList();
-                }
-
-                list.AddRange(discoveredDevices);
-            }
-
-            return list;
-        }
-
-        public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
-        {
-            foreach (var host in _liveTvManager.TunerHosts)
-            {
-                await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
-        {
-            var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
-
-            var configuredDevices = _config.GetLiveTvConfiguration().TunerHosts
-                .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
-                .ToList();
-
-            foreach (var device in discoveredDevices)
-            {
-                var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase));
-
-                if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase))
-                {
-                    _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url);
-
-                    configuredDevice.Url = device.Url;
-                    await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false);
-                }
-            }
-        }
-
-        private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken)
-        {
-            try
-            {
-                var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false);
-
-                foreach (var device in discoveredDevices)
-                {
-                    _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url);
-                }
-
-                return discoveredDevices;
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error discovering tuner devices");
-
-                return new List<TunerHostInfo>();
-            }
-        }
     }
     }
 }
 }

+ 6 - 0
src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs

@@ -1,4 +1,6 @@
 using Jellyfin.LiveTv.Channels;
 using Jellyfin.LiveTv.Channels;
+using Jellyfin.LiveTv.TunerHosts;
+using Jellyfin.LiveTv.TunerHosts.HdHomerun;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -21,5 +23,9 @@ public static class LiveTvServiceCollectionExtensions
         services.AddSingleton<ILiveTvManager, LiveTvManager>();
         services.AddSingleton<ILiveTvManager, LiveTvManager>();
         services.AddSingleton<IChannelManager, ChannelManager>();
         services.AddSingleton<IChannelManager, ChannelManager>();
         services.AddSingleton<IStreamHelper, StreamHelper>();
         services.AddSingleton<IStreamHelper, StreamHelper>();
+        services.AddSingleton<ITunerHostManager, TunerHostManager>();
+
+        services.AddSingleton<ITunerHost, HdHomerunHost>();
+        services.AddSingleton<ITunerHost, M3UTunerHost>();
     }
     }
 }
 }

+ 7 - 69
src/Jellyfin.LiveTv/LiveTvManager.cs

@@ -57,9 +57,9 @@ namespace Jellyfin.LiveTv
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IChannelManager _channelManager;
         private readonly IChannelManager _channelManager;
         private readonly LiveTvDtoService _tvDtoService;
         private readonly LiveTvDtoService _tvDtoService;
+        private readonly ITunerHostManager _tunerHostManager;
 
 
         private ILiveTvService[] _services = Array.Empty<ILiveTvService>();
         private ILiveTvService[] _services = Array.Empty<ILiveTvService>();
-        private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
         private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
         private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
 
 
         public LiveTvManager(
         public LiveTvManager(
@@ -74,7 +74,8 @@ namespace Jellyfin.LiveTv
             ILocalizationManager localization,
             ILocalizationManager localization,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IChannelManager channelManager,
             IChannelManager channelManager,
-            LiveTvDtoService liveTvDtoService)
+            LiveTvDtoService liveTvDtoService,
+            ITunerHostManager tunerHostManager)
         {
         {
             _config = config;
             _config = config;
             _logger = logger;
             _logger = logger;
@@ -88,6 +89,7 @@ namespace Jellyfin.LiveTv
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _channelManager = channelManager;
             _channelManager = channelManager;
             _tvDtoService = liveTvDtoService;
             _tvDtoService = liveTvDtoService;
+            _tunerHostManager = tunerHostManager;
         }
         }
 
 
         public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
         public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
@@ -104,8 +106,6 @@ namespace Jellyfin.LiveTv
         /// <value>The services.</value>
         /// <value>The services.</value>
         public IReadOnlyList<ILiveTvService> Services => _services;
         public IReadOnlyList<ILiveTvService> Services => _services;
 
 
-        public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts;
-
         public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders;
         public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders;
 
 
         public string GetEmbyTvActiveRecordingPath(string id)
         public string GetEmbyTvActiveRecordingPath(string id)
@@ -113,16 +113,10 @@ namespace Jellyfin.LiveTv
             return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
             return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
         }
         }
 
 
-        /// <summary>
-        /// Adds the parts.
-        /// </summary>
-        /// <param name="services">The services.</param>
-        /// <param name="tunerHosts">The tuner hosts.</param>
-        /// <param name="listingProviders">The listing providers.</param>
-        public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders)
+        /// <inheritdoc />
+        public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders)
         {
         {
             _services = services.ToArray();
             _services = services.ToArray();
-            _tunerHosts = tunerHosts.Where(i => i.IsSupported).ToArray();
 
 
             _listingProviders = listingProviders.ToArray();
             _listingProviders = listingProviders.ToArray();
 
 
@@ -154,20 +148,6 @@ namespace Jellyfin.LiveTv
                 }));
                 }));
         }
         }
 
 
-        public List<NameIdPair> GetTunerHostTypes()
-        {
-            return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
-            {
-                Name = i.Name,
-                Id = i.Type
-            }).ToList();
-        }
-
-        public Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
-        {
-            return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken);
-        }
-
         public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken)
         public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken)
         {
         {
             var user = query.UserId.Equals(default)
             var user = query.UserId.Equals(default)
@@ -1029,7 +1009,7 @@ namespace Jellyfin.LiveTv
         {
         {
             await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false);
             await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false);
 
 
-            await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
+            await _tunerHostManager.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
 
 
             var numComplete = 0;
             var numComplete = 0;
             double progressPerService = _services.Length == 0
             double progressPerService = _services.Length == 0
@@ -2166,48 +2146,6 @@ namespace Jellyfin.LiveTv
             return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
             return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
         }
         }
 
 
-        public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
-        {
-            info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
-
-            var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
-
-            if (provider is null)
-            {
-                throw new ResourceNotFoundException();
-            }
-
-            if (provider is IConfigurableTunerHost configurable)
-            {
-                await configurable.Validate(info).ConfigureAwait(false);
-            }
-
-            var config = _config.GetLiveTvConfiguration();
-
-            var list = config.TunerHosts.ToList();
-            var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
-
-            if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
-            {
-                info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-                list.Add(info);
-                config.TunerHosts = list.ToArray();
-            }
-            else
-            {
-                config.TunerHosts[index] = info;
-            }
-
-            _config.SaveConfiguration("livetv", config);
-
-            if (dataSourceChanged)
-            {
-                _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
-            }
-
-            return info;
-        }
-
         public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
         public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
         {
         {
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider

+ 181 - 0
src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs

@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Extensions;
+using Jellyfin.LiveTv.Configuration;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.LiveTv.TunerHosts;
+
+/// <inheritdoc />
+public class TunerHostManager : ITunerHostManager
+{
+    private const int TunerDiscoveryDurationMs = 3000;
+
+    private readonly ILogger<TunerHostManager> _logger;
+    private readonly IConfigurationManager _config;
+    private readonly ITaskManager _taskManager;
+    private readonly ITunerHost[] _tunerHosts;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="TunerHostManager"/> class.
+    /// </summary>
+    /// <param name="logger">The <see cref="ILogger{T}"/>.</param>
+    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+    /// <param name="taskManager">The <see cref="ITaskManager"/>.</param>
+    /// <param name="tunerHosts">The <see cref="IEnumerable{T}"/>.</param>
+    public TunerHostManager(
+        ILogger<TunerHostManager> logger,
+        IConfigurationManager config,
+        ITaskManager taskManager,
+        IEnumerable<ITunerHost> tunerHosts)
+    {
+        _logger = logger;
+        _config = config;
+        _taskManager = taskManager;
+        _tunerHosts = tunerHosts.Where(t => t.IsSupported).ToArray();
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts;
+
+    /// <inheritdoc />
+    public IEnumerable<NameIdPair> GetTunerHostTypes()
+        => _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
+        {
+            Name = i.Name,
+            Id = i.Type
+        });
+
+    /// <inheritdoc />
+    public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
+    {
+        info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info))!;
+
+        var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+        if (provider is null)
+        {
+            throw new ResourceNotFoundException();
+        }
+
+        if (provider is IConfigurableTunerHost configurable)
+        {
+            await configurable.Validate(info).ConfigureAwait(false);
+        }
+
+        var config = _config.GetLiveTvConfiguration();
+
+        var list = config.TunerHosts.ToList();
+        var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
+
+        if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
+        {
+            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            list.Add(info);
+            config.TunerHosts = list.ToArray();
+        }
+        else
+        {
+            config.TunerHosts[index] = info;
+        }
+
+        _config.SaveConfiguration("livetv", config);
+
+        if (dataSourceChanged)
+        {
+            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
+        }
+
+        return info;
+    }
+
+    /// <inheritdoc />
+    public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
+    {
+        var list = new List<TunerHostInfo>();
+
+        var configuredDeviceIds = _config.GetLiveTvConfiguration().TunerHosts
+            .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
+            .Select(i => i.DeviceId)
+            .ToList();
+
+        foreach (var host in _tunerHosts)
+        {
+            var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
+
+            if (newDevicesOnly)
+            {
+                discoveredDevices = discoveredDevices
+                    .Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase))
+                    .ToList();
+            }
+
+            list.AddRange(discoveredDevices);
+        }
+
+        return list;
+    }
+
+    /// <inheritdoc />
+    public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
+    {
+        foreach (var host in _tunerHosts)
+        {
+            await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
+        }
+    }
+
+    private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
+    {
+        var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
+
+        var configuredDevices = _config.GetLiveTvConfiguration().TunerHosts
+            .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
+            .ToList();
+
+        foreach (var device in discoveredDevices)
+        {
+            var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase));
+
+            if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase))
+            {
+                _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url);
+
+                configuredDevice.Url = device.Url;
+                await SaveTunerHost(configuredDevice).ConfigureAwait(false);
+            }
+        }
+    }
+
+    private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken)
+    {
+        try
+        {
+            var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false);
+
+            foreach (var device in discoveredDevices)
+            {
+                _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url);
+            }
+
+            return discoveredDevices;
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(ex, "Error discovering tuner devices");
+
+            return new List<TunerHostInfo>();
+        }
+    }
+}