mirror of https://github.com/jellyfin/jellyfin.git
Add ITunerHostManager service
This commit is contained in:
parent
449365182c
commit
9c2c066e6f
|
@ -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>());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
=> await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
|
||||||
return await _liveTvManager.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()
|
public IEnumerable<NameIdPair> GetTunerHostTypes()
|
||||||
{
|
=> _tunerHostManager.GetTunerHostTypes();
|
||||||
return _liveTvManager.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)
|
||||||
{
|
=> await _tunerHostManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
|
||||||
return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a live tv recording stream.
|
/// Gets a live tv recording stream.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
/// <inheritdoc />
|
||||||
/// Adds the parts.
|
public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders)
|
||||||
/// </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)
|
|
||||||
{
|
{
|
||||||
_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
|
||||||
|
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue