Add IListingsManager service

This commit is contained in:
Patrick Barron 2024-01-17 15:18:15 -05:00
parent 20f05f8103
commit 42b052a5a6
10 changed files with 590 additions and 530 deletions

View File

@ -45,6 +45,7 @@ public class LiveTvController : BaseJellyfinApiController
private readonly ILiveTvManager _liveTvManager;
private readonly IGuideManager _guideManager;
private readonly ITunerHostManager _tunerHostManager;
private readonly IListingsManager _listingsManager;
private readonly IUserManager _userManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
@ -59,6 +60,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
/// <param name="guideManager">Instance of the <see cref="IGuideManager"/> interface.</param>
/// <param name="tunerHostManager">Instance of the <see cref="ITunerHostManager"/> interface.</param>
/// <param name="listingsManager">Instance of the <see cref="IListingsManager"/> 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="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
@ -70,6 +72,7 @@ public class LiveTvController : BaseJellyfinApiController
ILiveTvManager liveTvManager,
IGuideManager guideManager,
ITunerHostManager tunerHostManager,
IListingsManager listingsManager,
IUserManager userManager,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
@ -81,6 +84,7 @@ public class LiveTvController : BaseJellyfinApiController
_liveTvManager = liveTvManager;
_guideManager = guideManager;
_tunerHostManager = tunerHostManager;
_listingsManager = listingsManager;
_userManager = userManager;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
@ -1015,7 +1019,7 @@ public class LiveTvController : BaseJellyfinApiController
listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
}
return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
return await _listingsManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
}
/// <summary>
@ -1029,7 +1033,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteListingProvider([FromQuery] string? id)
{
_liveTvManager.DeleteListingsProvider(id);
_listingsManager.DeleteListingsProvider(id);
return NoContent();
}
@ -1050,9 +1054,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] string? type,
[FromQuery] string? location,
[FromQuery] string? country)
{
return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
}
=> await _listingsManager.GetLineups(type, id, country, location).ConfigureAwait(false);
/// <summary>
/// Gets available countries.
@ -1083,48 +1085,20 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("ChannelMappingOptions")]
[Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
{
var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
.ConfigureAwait(false);
var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
.ConfigureAwait(false);
var mappings = listingsProviderInfo.ChannelMappings;
return new ChannelMappingOptionsDto
{
TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
ProviderChannels = providerChannels.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.Id
}).ToList(),
Mappings = mappings,
ProviderName = listingsProviderName
};
}
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
=> _listingsManager.GetChannelMappingOptions(providerId);
/// <summary>
/// Set channel mappings.
/// </summary>
/// <param name="setChannelMappingDto">The set channel mapping dto.</param>
/// <param name="dto">The set channel mapping dto.</param>
/// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
{
return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
}
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
/// <summary>
/// Get tuner host types.

View File

@ -0,0 +1,79 @@
using System;
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 <see cref="IListingsProvider"/>s and mapping
/// their channels to channels provided by <see cref="ITunerHost"/>s.
/// </summary>
public interface IListingsManager
{
/// <summary>
/// Saves the listing provider.
/// </summary>
/// <param name="info">The listing provider information.</param>
/// <param name="validateLogin">A value indicating whether to validate login.</param>
/// <param name="validateListings">A value indicating whether to validate listings..</param>
/// <returns>Task.</returns>
Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings);
/// <summary>
/// Deletes the listing provider.
/// </summary>
/// <param name="id">The listing provider's id.</param>
void DeleteListingsProvider(string? id);
/// <summary>
/// Gets the lineups.
/// </summary>
/// <param name="providerType">Type of the provider.</param>
/// <param name="providerId">The provider identifier.</param>
/// <param name="country">The country.</param>
/// <param name="location">The location.</param>
/// <returns>The available lineups.</returns>
Task<List<NameIdPair>> GetLineups(string? providerType, string? providerId, string? country, string? location);
/// <summary>
/// Gets the programs for a provided channel.
/// </summary>
/// <param name="channel">The channel to retrieve programs for.</param>
/// <param name="startDateUtc">The earliest date to retrieve programs for.</param>
/// <param name="endDateUtc">The latest date to retrieve programs for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
/// <returns>The available programs.</returns>
Task<IEnumerable<ProgramInfo>> GetProgramsAsync(
ChannelInfo channel,
DateTime startDateUtc,
DateTime endDateUtc,
CancellationToken cancellationToken);
/// <summary>
/// Adds metadata from the <see cref="IListingsProvider"/>s to the provided channels.
/// </summary>
/// <param name="channels">The channels.</param>
/// <param name="enableCache">A value indicating whether to use the EPG channel cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
/// <returns>A task representing the metadata population.</returns>
Task AddProviderMetadata(IList<ChannelInfo> channels, bool enableCache, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel mapping options for a provider.
/// </summary>
/// <param name="providerId">The id of the provider to use.</param>
/// <returns>The channel mapping options.</returns>
Task<ChannelMappingOptionsDto> GetChannelMappingOptions(string? providerId);
/// <summary>
/// Sets the channel mapping.
/// </summary>
/// <param name="providerId">The id of the provider for the mapping.</param>
/// <param name="tunerChannelNumber">The tuner channel number.</param>
/// <param name="providerChannelNumber">The provider channel number.</param>
/// <returns>The updated channel mapping.</returns>
Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
}

View File

@ -36,8 +36,6 @@ namespace MediaBrowser.Controller.LiveTv
/// <value>The services.</value>
IReadOnlyList<ILiveTvService> Services { get; }
IReadOnlyList<IListingsProvider> ListingProviders { get; }
/// <summary>
/// Gets the new timer defaults asynchronous.
/// </summary>
@ -239,31 +237,6 @@ namespace MediaBrowser.Controller.LiveTv
/// <returns>Task.</returns>
Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
/// <summary>
/// Saves the listing provider.
/// </summary>
/// <param name="info">The information.</param>
/// <param name="validateLogin">if set to <c>true</c> [validate login].</param>
/// <param name="validateListings">if set to <c>true</c> [validate listings].</param>
/// <returns>Task.</returns>
Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings);
void DeleteListingsProvider(string id);
Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels);
/// <summary>
/// Gets the lineups.
/// </summary>
/// <param name="providerType">Type of the provider.</param>
/// <param name="providerId">The provider identifier.</param>
/// <param name="country">The country.</param>
/// <param name="location">The location.</param>
/// <returns>Task&lt;List&lt;NameIdPair&gt;&gt;.</returns>
Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location);
/// <summary>
/// Adds the channel information.
/// </summary>
@ -272,10 +245,6 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="user">The user.</param>
void AddChannelInfo(IReadOnlyCollection<(BaseItemDto ItemDto, LiveTvChannel Channel)> items, DtoOptions options, User user);
Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
string GetEmbyTvActiveRecordingPath(string id);
ActiveRecordingInfo GetActiveRecordingInfo(string path);

View File

@ -1,17 +0,0 @@
#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Controller.LiveTv
{
public class TunerChannelMapping
{
public string Name { get; set; }
public string ProviderChannelName { get; set; }
public string ProviderChannelId { get; set; }
public string Id { get; set; }
}
}

View File

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
namespace Jellyfin.Api.Models.LiveTvDtos;
namespace MediaBrowser.Model.LiveTv;
/// <summary>
/// Channel mapping options dto.

View File

@ -0,0 +1,16 @@
#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Model.LiveTv;
public class TunerChannelMapping
{
public string Name { get; set; }
public string ProviderChannelName { get; set; }
public string ProviderChannelId { get; set; }
public string Id { get; set; }
}

View File

@ -61,14 +61,11 @@ namespace Jellyfin.LiveTv.EmbyTV
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IStreamHelper _streamHelper;
private readonly LiveTvDtoService _tvDtoService;
private readonly IListingsProvider[] _listingsProviders;
private readonly IListingsManager _listingsManager;
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _recordingDeleteSemaphore = new(1);
private bool _disposed;
@ -86,7 +83,7 @@ namespace Jellyfin.LiveTv.EmbyTV
IProviderManager providerManager,
IMediaEncoder mediaEncoder,
LiveTvDtoService tvDtoService,
IEnumerable<IListingsProvider> listingsProviders)
IListingsManager listingsManager)
{
Current = this;
@ -102,7 +99,7 @@ namespace Jellyfin.LiveTv.EmbyTV
_tunerHostManager = tunerHostManager;
_mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper;
_listingsProviders = listingsProviders.ToArray();
_listingsManager = listingsManager;
_seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json"));
@ -312,15 +309,15 @@ namespace Jellyfin.LiveTv.EmbyTV
private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
{
var list = new List<ChannelInfo>();
var channels = new List<ChannelInfo>();
foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
var tunerChannels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
list.AddRange(channels);
channels.AddRange(tunerChannels);
}
catch (Exception ex)
{
@ -328,209 +325,9 @@ namespace Jellyfin.LiveTv.EmbyTV
}
}
foreach (var provider in GetListingProviders())
{
var enabledChannels = list
.Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId))
.ToList();
await _listingsManager.AddProviderMetadata(channels, enableCache, cancellationToken).ConfigureAwait(false);
if (enabledChannels.Count > 0)
{
try
{
await AddMetadata(provider.Item1, provider.Item2, enabledChannels, enableCache, cancellationToken).ConfigureAwait(false);
}
catch (NotSupportedException)
{
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding metadata");
}
}
}
return list;
}
private async Task AddMetadata(
IListingsProvider provider,
ListingsProviderInfo info,
IEnumerable<ChannelInfo> tunerChannels,
bool enableCache,
CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
foreach (var tunerChannel in tunerChannels)
{
var epgChannel = GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
if (epgChannel is not null)
{
if (!string.IsNullOrWhiteSpace(epgChannel.Name))
{
// tunerChannel.Name = epgChannel.Name;
}
if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
{
tunerChannel.ImageUrl = epgChannel.ImageUrl;
}
}
}
}
private async Task<EpgChannelData> GetEpgChannels(
IListingsProvider provider,
ListingsProviderInfo info,
bool enableCache,
CancellationToken cancellationToken)
{
if (!enableCache || !_epgChannels.TryGetValue(info.Id, out var result))
{
var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
foreach (var channel in channels)
{
_logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id);
}
result = new EpgChannelData(channels);
_epgChannels.AddOrUpdate(info.Id, result, (_, _) => result);
}
return result;
}
private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false);
return GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
}
private static string GetMappedChannel(string channelId, NameValuePair[] mappings)
{
foreach (NameValuePair mapping in mappings)
{
if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
{
return mapping.Value;
}
}
return channelId;
}
internal ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
{
return GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(epgChannels));
}
private ChannelInfo GetEpgChannelFromTunerChannel(ListingsProviderInfo info, ChannelInfo tunerChannel, EpgChannelData epgChannels)
{
return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
}
private ChannelInfo GetEpgChannelFromTunerChannel(
NameValuePair[] mappings,
ChannelInfo tunerChannel,
EpgChannelData epgChannelData)
{
if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
{
var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
{
mappedTunerChannelId = tunerChannel.Id;
}
var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
if (channel is not null)
{
return channel;
}
}
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
{
var tunerChannelId = tunerChannel.TunerChannelId;
if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
{
tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
}
var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
{
mappedTunerChannelId = tunerChannelId;
}
var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
if (channel is not null)
{
return channel;
}
}
if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
{
var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
if (string.IsNullOrWhiteSpace(tunerChannelNumber))
{
tunerChannelNumber = tunerChannel.Number;
}
var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber);
if (channel is not null)
{
return channel;
}
}
if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
{
var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
var channel = epgChannelData.GetChannelByName(normalizedName);
if (channel is not null)
{
return channel;
}
}
return null;
}
public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
{
var list = new List<ChannelInfo>();
foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
list.AddRange(channels);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting channels");
}
}
return list
.Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId))
.ToList();
return channels;
}
public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
@ -877,75 +674,13 @@ namespace Jellyfin.LiveTv.EmbyTV
return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
}
private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
{
if (info.EnableAllTuners)
{
return true;
}
if (string.IsNullOrWhiteSpace(tunerHostId))
{
throw new ArgumentNullException(nameof(tunerHostId));
}
return info.EnabledTuners.Contains(tunerHostId, StringComparison.OrdinalIgnoreCase);
}
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
{
var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
foreach (var provider in GetListingProviders())
{
if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId))
{
_logger.LogDebug("Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner.", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
continue;
}
_logger.LogDebug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
if (epgChannel is null)
{
_logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
continue;
}
List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
.ConfigureAwait(false)).ToList();
// Replace the value that came from the provider with a normalized value
foreach (var program in programs)
{
program.ChannelId = channelId;
program.Id += "_" + channelId;
}
if (programs.Count > 0)
{
return programs;
}
}
return Enumerable.Empty<ProgramInfo>();
}
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
{
return _config.GetLiveTvConfiguration().ListingProviders
.Select(i =>
{
var provider = _listingsProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
return provider is null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
})
.Where(i => i is not null)
.ToList();
return await _listingsManager.GetProgramsAsync(channel, startDateUtc, endDateUtc, cancellationToken)
.ConfigureAwait(false);
}
public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)

View File

@ -26,6 +26,7 @@ public static class LiveTvServiceCollectionExtensions
services.AddSingleton<IChannelManager, ChannelManager>();
services.AddSingleton<IStreamHelper, StreamHelper>();
services.AddSingleton<ITunerHostManager, TunerHostManager>();
services.AddSingleton<IListingsManager, ListingsManager>();
services.AddSingleton<IGuideManager, GuideManager>();
services.AddSingleton<ILiveTvService, EmbyTV.EmbyTV>();

View File

@ -0,0 +1,470 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.LiveTv.Configuration;
using Jellyfin.LiveTv.EmbyTV;
using Jellyfin.LiveTv.Guide;
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.Listings;
/// <inheritdoc />
public class ListingsManager : IListingsManager
{
private readonly ILogger<ListingsManager> _logger;
private readonly IConfigurationManager _config;
private readonly ITaskManager _taskManager;
private readonly ITunerHostManager _tunerHostManager;
private readonly IListingsProvider[] _listingsProviders;
private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Initializes a new instance of the <see cref="ListingsManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
/// <param name="config">The <see cref="IConfigurationManager"/>.</param>
/// <param name="taskManager">The <see cref="ITaskManager"/>.</param>
/// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
/// <param name="listingsProviders">The <see cref="IListingsProvider"/>.</param>
public ListingsManager(
ILogger<ListingsManager> logger,
IConfigurationManager config,
ITaskManager taskManager,
ITunerHostManager tunerHostManager,
IEnumerable<IListingsProvider> listingsProviders)
{
_logger = logger;
_config = config;
_taskManager = taskManager;
_tunerHostManager = tunerHostManager;
_listingsProviders = listingsProviders.ToArray();
}
/// <inheritdoc />
public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
{
ArgumentNullException.ThrowIfNull(info);
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.SerializeToUtf8Bytes(info))!;
var provider = GetProvider(info.Type);
await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
var config = _config.GetLiveTvConfiguration();
var list = config.ListingProviders.ToList();
int 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.ListingProviders = list.ToArray();
}
else
{
config.ListingProviders[index] = info;
}
_config.SaveConfiguration("livetv", config);
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return info;
}
/// <inheritdoc />
public void DeleteListingsProvider(string? id)
{
var config = _config.GetLiveTvConfiguration();
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_config.SaveConfiguration("livetv", config);
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
/// <inheritdoc />
public Task<List<NameIdPair>> GetLineups(string? providerType, string? providerId, string? country, string? location)
{
if (string.IsNullOrWhiteSpace(providerId))
{
return GetProvider(providerType).GetLineups(null, country, location);
}
var info = _config.GetLiveTvConfiguration().ListingProviders
.FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase))
?? throw new ResourceNotFoundException();
return GetProvider(info.Type).GetLineups(info, country, location);
}
/// <inheritdoc />
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(
ChannelInfo channel,
DateTime startDateUtc,
DateTime endDateUtc,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(channel);
foreach (var (provider, providerInfo) in GetListingProviders())
{
if (!IsListingProviderEnabledForTuner(providerInfo, channel.TunerHostId))
{
_logger.LogDebug(
"Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner.",
channel.Number,
channel.Name,
provider.Name,
providerInfo.ListingsId ?? string.Empty);
continue;
}
_logger.LogDebug(
"Getting programs for channel {0}-{1} from {2}-{3}",
channel.Number,
channel.Name,
provider.Name,
providerInfo.ListingsId ?? string.Empty);
var epgChannels = await GetEpgChannels(provider, providerInfo, true, cancellationToken).ConfigureAwait(false);
var epgChannel = GetEpgChannelFromTunerChannel(providerInfo.ChannelMappings, channel, epgChannels);
if (epgChannel is null)
{
_logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Name, providerInfo.ListingsId ?? string.Empty);
continue;
}
var programs = (await provider
.GetProgramsAsync(providerInfo, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken).ConfigureAwait(false))
.ToList();
// Replace the value that came from the provider with a normalized value
foreach (var program in programs)
{
program.ChannelId = channel.Id;
program.Id += "_" + channel.Id;
}
if (programs.Count > 0)
{
return programs;
}
}
return Enumerable.Empty<ProgramInfo>();
}
/// <inheritdoc />
public async Task AddProviderMetadata(IList<ChannelInfo> channels, bool enableCache, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(channels);
foreach (var (provider, providerInfo) in GetListingProviders())
{
var enabledChannels = channels
.Where(i => IsListingProviderEnabledForTuner(providerInfo, i.TunerHostId))
.ToList();
if (enabledChannels.Count == 0)
{
continue;
}
try
{
await AddMetadata(provider, providerInfo, enabledChannels, enableCache, cancellationToken).ConfigureAwait(false);
}
catch (NotSupportedException)
{
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding metadata");
}
}
}
/// <inheritdoc />
public async Task<ChannelMappingOptionsDto> GetChannelMappingOptions(string? providerId)
{
var listingsProviderInfo = _config.GetLiveTvConfiguration().ListingProviders
.First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
var provider = GetProvider(listingsProviderInfo.Type);
var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
.ConfigureAwait(false);
var providerChannels = await provider.GetChannels(listingsProviderInfo, default)
.ConfigureAwait(false);
var mappings = listingsProviderInfo.ChannelMappings;
return new ChannelMappingOptionsDto
{
TunerChannels = tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
ProviderChannels = providerChannels.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.Id
}).ToList(),
Mappings = mappings,
ProviderName = provider.Name
};
}
/// <inheritdoc />
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
{
var config = _config.GetLiveTvConfiguration();
var listingsProviderInfo = config.ListingProviders
.First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings
.Where(pair => !string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
{
var list = listingsProviderInfo.ChannelMappings.ToList();
list.Add(new NameValuePair
{
Name = tunerChannelNumber,
Value = providerChannelNumber
});
listingsProviderInfo.ChannelMappings = list.ToArray();
}
_config.SaveConfiguration("livetv", config);
var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
.ConfigureAwait(false);
var providerChannels = await GetProvider(listingsProviderInfo.Type).GetChannels(listingsProviderInfo, default)
.ConfigureAwait(false);
var tunerChannelMappings = tunerChannels
.Select(i => GetTunerChannelMapping(i, listingsProviderInfo.ChannelMappings, providerChannels)).ToList();
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
}
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
=> _config.GetLiveTvConfiguration().ListingProviders
.Select(i =>
{
var provider = _listingsProviders
.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
return provider is null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
})
.Where(i => i is not null)
.ToList()!; // Already filtered out null
private async Task AddMetadata(
IListingsProvider provider,
ListingsProviderInfo info,
IEnumerable<ChannelInfo> tunerChannels,
bool enableCache,
CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
foreach (var tunerChannel in tunerChannels)
{
var epgChannel = GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
if (epgChannel is null)
{
continue;
}
if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
{
tunerChannel.ImageUrl = epgChannel.ImageUrl;
}
}
}
private static bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
{
if (info.EnableAllTuners)
{
return true;
}
ArgumentException.ThrowIfNullOrWhiteSpace(tunerHostId);
return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
}
private static string GetMappedChannel(string channelId, NameValuePair[] mappings)
{
foreach (NameValuePair mapping in mappings)
{
if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
{
return mapping.Value;
}
}
return channelId;
}
private async Task<EpgChannelData> GetEpgChannels(
IListingsProvider provider,
ListingsProviderInfo info,
bool enableCache,
CancellationToken cancellationToken)
{
if (enableCache && _epgChannels.TryGetValue(info.Id, out var result))
{
return result;
}
var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
foreach (var channel in channels)
{
_logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id);
}
result = new EpgChannelData(channels);
_epgChannels.AddOrUpdate(info.Id, result, (_, _) => result);
return result;
}
private static ChannelInfo? GetEpgChannelFromTunerChannel(
NameValuePair[] mappings,
ChannelInfo tunerChannel,
EpgChannelData epgChannelData)
{
if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
{
var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
{
mappedTunerChannelId = tunerChannel.Id;
}
var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
if (channel is not null)
{
return channel;
}
}
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
{
var tunerChannelId = tunerChannel.TunerChannelId;
if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
{
tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
}
var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
{
mappedTunerChannelId = tunerChannelId;
}
var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
if (channel is not null)
{
return channel;
}
}
if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
{
var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
if (string.IsNullOrWhiteSpace(tunerChannelNumber))
{
tunerChannelNumber = tunerChannel.Number;
}
var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber);
if (channel is not null)
{
return channel;
}
}
if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
{
var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
var channel = epgChannelData.GetChannelByName(normalizedName);
if (channel is not null)
{
return channel;
}
}
return null;
}
private static TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, IList<ChannelInfo> providerChannels)
{
var result = new TunerChannelMapping
{
Name = tunerChannel.Name,
Id = tunerChannel.Id
};
if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
{
result.Name = tunerChannel.Number + " " + result.Name;
}
var providerChannel = GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(providerChannels));
if (providerChannel is not null)
{
result.ProviderChannelName = providerChannel.Name;
result.ProviderChannelId = providerChannel.Id;
}
return result;
}
private async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo info, CancellationToken cancellationToken)
{
var channels = new List<ChannelInfo>();
foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
var tunerChannels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
channels.AddRange(tunerChannels.Where(channel => IsListingProviderEnabledForTuner(info, channel.TunerHostId)));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting channels");
}
}
return channels;
}
private IListingsProvider GetProvider(string? providerType)
=> _listingsProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase))
?? throw new ResourceNotFoundException($"Couldn't find provider of type {providerType}");
}

View File

@ -6,14 +6,12 @@ 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.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.LiveTv.Configuration;
using Jellyfin.LiveTv.Guide;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@ -27,7 +25,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Jellyfin.LiveTv
@ -43,12 +40,10 @@ namespace Jellyfin.LiveTv
private readonly IDtoService _dtoService;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization;
private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
private readonly ILiveTvService[] _services;
private readonly IListingsProvider[] _listingProviders;
public LiveTvManager(
IServerConfigurationManager config,
@ -57,25 +52,21 @@ namespace Jellyfin.LiveTv
IDtoService dtoService,
IUserManager userManager,
ILibraryManager libraryManager,
ITaskManager taskManager,
ILocalizationManager localization,
IChannelManager channelManager,
LiveTvDtoService liveTvDtoService,
IEnumerable<ILiveTvService> services,
IEnumerable<IListingsProvider> listingProviders)
IEnumerable<ILiveTvService> services)
{
_config = config;
_logger = logger;
_userManager = userManager;
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
_dtoService = dtoService;
_userDataManager = userDataManager;
_channelManager = channelManager;
_tvDtoService = liveTvDtoService;
_services = services.ToArray();
_listingProviders = listingProviders.ToArray();
var defaultService = _services.OfType<EmbyTV.EmbyTV>().First();
defaultService.TimerCreated += OnEmbyTvTimerCreated;
@ -96,8 +87,6 @@ namespace Jellyfin.LiveTv
/// <value>The services.</value>
public IReadOnlyList<ILiveTvService> Services => _services;
public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders;
public string GetEmbyTvActiveRecordingPath(string id)
{
return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
@ -1465,161 +1454,6 @@ namespace Jellyfin.LiveTv
return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
}
public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
{
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
if (provider is null)
{
throw new ResourceNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Couldn't find provider of type: '{0}'",
info.Type));
}
await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
var config = _config.GetLiveTvConfiguration();
var list = config.ListingProviders.ToList();
int 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.ListingProviders = list.ToArray();
}
else
{
config.ListingProviders[index] = info;
}
_config.SaveConfiguration("livetv", config);
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return info;
}
public void DeleteListingsProvider(string id)
{
var config = _config.GetLiveTvConfiguration();
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_config.SaveConfiguration("livetv", config);
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
{
var config = _config.GetLiveTvConfiguration();
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
{
var list = listingsProviderInfo.ChannelMappings.ToList();
list.Add(new NameValuePair
{
Name = tunerChannelNumber,
Value = providerChannelNumber
});
listingsProviderInfo.ChannelMappings = list.ToArray();
}
_config.SaveConfiguration("livetv", config);
var tunerChannels = await GetChannelsForListingsProvider(providerId, CancellationToken.None)
.ConfigureAwait(false);
var providerChannels = await GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
.ConfigureAwait(false);
var mappings = listingsProviderInfo.ChannelMappings;
var tunerChannelMappings =
tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
}
public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels)
{
var result = new TunerChannelMapping
{
Name = tunerChannel.Name,
Id = tunerChannel.Id
};
if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
{
result.Name = tunerChannel.Number + " " + result.Name;
}
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels);
if (providerChannel is not null)
{
result.ProviderChannelName = providerChannel.Name;
result.ProviderChannelId = providerChannel.Id;
}
return result;
}
public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location)
{
var config = _config.GetLiveTvConfiguration();
if (string.IsNullOrWhiteSpace(providerId))
{
var provider = _listingProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase));
if (provider is null)
{
throw new ResourceNotFoundException();
}
return provider.GetLineups(null, country, location);
}
else
{
var info = config.ListingProviders.FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
if (provider is null)
{
throw new ResourceNotFoundException();
}
return provider.GetLineups(info, country, location);
}
}
public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken)
{
var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken);
}
public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken)
{
var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase));
return provider.GetChannels(info, cancellationToken);
}
/// <inheritdoc />
public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
=> GetRecordingFoldersAsync(user, false);