#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.LiveTv.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; namespace Jellyfin.LiveTv { /// /// Class LiveTvManager. /// public class LiveTvManager : ILiveTvManager { private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; private readonly IUserDataManager _userDataManager; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IChannelManager _channelManager; private readonly LiveTvDtoService _tvDtoService; private readonly ILiveTvService[] _services; public LiveTvManager( IServerConfigurationManager config, ILogger logger, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization, IChannelManager channelManager, LiveTvDtoService liveTvDtoService, IEnumerable services) { _config = config; _logger = logger; _userManager = userManager; _libraryManager = libraryManager; _localization = localization; _dtoService = dtoService; _userDataManager = userDataManager; _channelManager = channelManager; _tvDtoService = liveTvDtoService; _services = services.ToArray(); var defaultService = _services.OfType().First(); defaultService.TimerCreated += OnEmbyTvTimerCreated; defaultService.TimerCancelled += OnEmbyTvTimerCancelled; } public event EventHandler> SeriesTimerCancelled; public event EventHandler> TimerCancelled; public event EventHandler> TimerCreated; public event EventHandler> SeriesTimerCreated; /// /// Gets the services. /// /// The services. public IReadOnlyList Services => _services; public string GetEmbyTvActiveRecordingPath(string id) { return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); } private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs e) { var timerId = e.Argument; TimerCancelled?.Invoke(this, new GenericEventArgs(new TimerEventInfo(timerId))); } private void OnEmbyTvTimerCreated(object sender, GenericEventArgs e) { var timer = e.Argument; TimerCreated?.Invoke(this, new GenericEventArgs( new TimerEventInfo(timer.Id) { ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId) })); } public QueryResult GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) { var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); var topFolder = GetInternalLiveTvFolder(cancellationToken); var internalQuery = new InternalItemsQuery(user) { IsMovie = query.IsMovie, IsNews = query.IsNews, IsKids = query.IsKids, IsSports = query.IsSports, IsSeries = query.IsSeries, IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, TopParentIds = new[] { topFolder.Id }, IsFavorite = query.IsFavorite, IsLiked = query.IsLiked, StartIndex = query.StartIndex, Limit = query.Limit, DtoOptions = dtoOptions }; var orderBy = internalQuery.OrderBy.ToList(); orderBy.AddRange(query.SortBy.Select(i => (i, query.SortOrder ?? SortOrder.Ascending))); if (query.EnableFavoriteSorting) { orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending)); } if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName)) { orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending)); } internalQuery.OrderBy = orderBy.ToArray(); return _libraryManager.GetItemsResult(internalQuery); } public async Task> GetChannelStream(string id, string mediaSourceId, List currentLiveStreams, CancellationToken cancellationToken) { if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) { mediaSourceId = null; } var channel = (LiveTvChannel)_libraryManager.GetItemById(id); bool isVideo = channel.ChannelType == ChannelType.TV; var service = GetService(channel); _logger.LogInformation("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId); MediaSourceInfo info; #pragma warning disable CA1859 // TODO: Analyzer bug? ILiveStream liveStream; #pragma warning restore CA1859 if (service is ISupportsDirectStreamProvider supportsManagedStream) { liveStream = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); info = liveStream.MediaSource; } else { info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false); var openedId = info.Id; Func closeFn = () => service.CloseLiveStream(openedId, CancellationToken.None); liveStream = new ExclusiveLiveStream(info, closeFn); var startTime = DateTime.UtcNow; await liveStream.Open(cancellationToken).ConfigureAwait(false); var endTime = DateTime.UtcNow; _logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds); } info.RequiresClosing = true; var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_"; info.LiveStreamId = idPrefix + info.Id; Normalize(info, service, isVideo); return new Tuple(info, liveStream); } public async Task> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken) { var baseItem = (LiveTvChannel)item; var service = GetService(baseItem); var sources = await service.GetChannelStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false); if (sources.Count == 0) { throw new NotImplementedException(); } foreach (var source in sources) { Normalize(source, service, baseItem.ChannelType == ChannelType.TV); } return sources; } private ILiveTvService GetService(LiveTvChannel item) { var name = item.ServiceName; return GetService(name); } private ILiveTvService GetService(LiveTvProgram item) { var channel = _libraryManager.GetItemById(item.ChannelId) as LiveTvChannel; return GetService(channel); } private ILiveTvService GetService(string name) => Array.Find(_services, x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)) ?? throw new KeyNotFoundException( string.Format( CultureInfo.InvariantCulture, "No service with the name '{0}' can be found.", name)); private static void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo) { // Not all of the plugins are setting this mediaSource.IsInfiniteStream = true; if (mediaSource.MediaStreams.Count == 0) { if (isVideo) { mediaSource.MediaStreams = new MediaStream[] { new MediaStream { Type = MediaStreamType.Video, // Set the index to -1 because we don't know the exact index of the video stream within the container Index = -1, // Set to true if unknown to enable deinterlacing IsInterlaced = true }, new MediaStream { Type = MediaStreamType.Audio, // Set the index to -1 because we don't know the exact index of the audio stream within the container Index = -1 } }; } else { mediaSource.MediaStreams = new MediaStream[] { new MediaStream { Type = MediaStreamType.Audio, // Set the index to -1 because we don't know the exact index of the audio stream within the container Index = -1 } }; } } // Clean some bad data coming from providers foreach (var stream in mediaSource.MediaStreams) { if (stream.BitRate.HasValue && stream.BitRate <= 0) { stream.BitRate = null; } if (stream.Channels.HasValue && stream.Channels <= 0) { stream.Channels = null; } if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0) { stream.AverageFrameRate = null; } if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0) { stream.RealFrameRate = null; } if (stream.Width.HasValue && stream.Width <= 0) { stream.Width = null; } if (stream.Height.HasValue && stream.Height <= 0) { stream.Height = null; } if (stream.SampleRate.HasValue && stream.SampleRate <= 0) { stream.SampleRate = null; } if (stream.Level.HasValue && stream.Level <= 0) { stream.Level = null; } } var indexes = mediaSource.MediaStreams.Select(i => i.Index).Distinct().ToList(); // If there are duplicate stream indexes, set them all to unknown if (indexes.Count != mediaSource.MediaStreams.Count) { foreach (var stream in mediaSource.MediaStreams) { stream.Index = -1; } } // Set the total bitrate if not already supplied mediaSource.InferTotalBitrate(); if (service is not EmbyTV.EmbyTV) { // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says // mediaSource.SupportsDirectPlay = false; // mediaSource.SupportsDirectStream = false; mediaSource.SupportsTranscoding = true; foreach (var stream in mediaSource.MediaStreams) { if (stream.Type == MediaStreamType.Video && string.IsNullOrWhiteSpace(stream.NalLengthSize)) { stream.NalLengthSize = "0"; } if (stream.Type == MediaStreamType.Video) { stream.IsInterlaced = true; } } } } public async Task GetProgram(string id, CancellationToken cancellationToken, User user = null) { var program = _libraryManager.GetItemById(id); var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); var list = new List<(BaseItemDto ItemDto, string ExternalId, string ExternalSeriesId)> { (dto, program.ExternalId, program.ExternalSeriesId) }; await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false); return dto; } public async Task> GetPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { var user = query.User; var topFolder = GetInternalLiveTvFolder(cancellationToken); if (query.OrderBy.Count == 0) { // Unless something else was specified, order by start date to take advantage of a specialized index query.OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }; } RemoveFields(options); var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, MinEndDate = query.MinEndDate, MinStartDate = query.MinStartDate, MaxEndDate = query.MaxEndDate, MaxStartDate = query.MaxStartDate, ChannelIds = query.ChannelIds, IsMovie = query.IsMovie, IsSeries = query.IsSeries, IsSports = query.IsSports, IsKids = query.IsKids, IsNews = query.IsNews, Genres = query.Genres, GenreIds = query.GenreIds, StartIndex = query.StartIndex, Limit = query.Limit, OrderBy = query.OrderBy, EnableTotalRecordCount = query.EnableTotalRecordCount, TopParentIds = new[] { topFolder.Id }, Name = query.Name, DtoOptions = options, HasAired = query.HasAired, IsAiring = query.IsAiring }; if (!string.IsNullOrWhiteSpace(query.SeriesTimerId)) { var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer is not null) { internalQuery.ExternalSeriesId = seriesTimer.SeriesId; if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId)) { // Better to return nothing than every program in the database return new QueryResult(); } } else { // Better to return nothing than every program in the database return new QueryResult(); } } var queryResult = _libraryManager.QueryItems(internalQuery); var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); return new QueryResult( query.StartIndex, queryResult.TotalRecordCount, returnArray); } public QueryResult GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { var user = query.User; var topFolder = GetInternalLiveTvFolder(cancellationToken); var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, IsAiring = query.IsAiring, HasAired = query.HasAired, IsNews = query.IsNews, IsMovie = query.IsMovie, IsSeries = query.IsSeries, IsSports = query.IsSports, IsKids = query.IsKids, EnableTotalRecordCount = query.EnableTotalRecordCount, OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }, TopParentIds = new[] { topFolder.Id }, DtoOptions = options, GenreIds = query.GenreIds }; if (query.Limit.HasValue) { internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200); } var programList = _libraryManager.QueryItems(internalQuery).Items; var totalCount = programList.Count; var orderedPrograms = programList.Cast().OrderBy(i => i.StartDate.Date); if (query.IsAiring ?? false) { orderedPrograms = orderedPrograms .ThenByDescending(i => GetRecommendationScore(i, user, true)); } IEnumerable programs = orderedPrograms; if (query.Limit.HasValue) { programs = programs.Take(query.Limit.Value); } return new QueryResult( query.StartIndex, totalCount, programs.ToArray()); } public Task> GetRecommendedProgramsAsync(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { if (!(query.IsAiring ?? false)) { return GetPrograms(query, options, cancellationToken); } RemoveFields(options); var internalResult = GetRecommendedProgramsInternal(query, options, cancellationToken); return Task.FromResult(new QueryResult( query.StartIndex, internalResult.TotalRecordCount, _dtoService.GetBaseItemDtos(internalResult.Items, options, query.User))); } private int GetRecommendationScore(LiveTvProgram program, User user, bool factorChannelWatchCount) { var score = 0; if (program.IsLive) { score++; } if (program.IsSeries && !program.IsRepeat) { score++; } var channel = _libraryManager.GetItemById(program.ChannelId); if (channel is null) { return score; } var channelUserdata = _userDataManager.GetUserData(user, channel); if (channelUserdata.Likes.HasValue) { score += channelUserdata.Likes.Value ? 2 : -2; } if (channelUserdata.IsFavorite) { score += 3; } if (factorChannelWatchCount) { score += channelUserdata.PlayCount; } return score; } private async Task AddRecordingInfo(IEnumerable<(BaseItemDto ItemDto, string ExternalId, string ExternalSeriesId)> programs, CancellationToken cancellationToken) { IReadOnlyList timerList = null; IReadOnlyList seriesTimerList = null; foreach (var programTuple in programs) { var program = programTuple.ItemDto; var externalProgramId = programTuple.ExternalId; string externalSeriesId = programTuple.ExternalSeriesId; timerList ??= (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items; var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase)); var foundSeriesTimer = false; if (timer is not null) { if (timer.Status != RecordingStatus.Cancelled && timer.Status != RecordingStatus.Error) { program.TimerId = _tvDtoService.GetInternalTimerId(timer.Id); program.Status = timer.Status.ToString(); } if (!string.IsNullOrEmpty(timer.SeriesTimerId)) { program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.SeriesTimerId) .ToString("N", CultureInfo.InvariantCulture); foundSeriesTimer = true; } } if (foundSeriesTimer || string.IsNullOrWhiteSpace(externalSeriesId)) { continue; } seriesTimerList ??= (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items; var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer is not null) { program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id) .ToString("N", CultureInfo.InvariantCulture); } } } private async Task> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user) { if (user is null) { return new QueryResult(); } var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false); var folderIds = Array.ConvertAll(folders, x => x.Id); var excludeItemTypes = new List(); if (folderIds.Length == 0) { return new QueryResult(); } var includeItemTypes = new List(); var genres = new List(); if (query.IsMovie.HasValue) { if (query.IsMovie.Value) { includeItemTypes.Add(BaseItemKind.Movie); } else { excludeItemTypes.Add(BaseItemKind.Movie); } } if (query.IsSeries.HasValue) { if (query.IsSeries.Value) { includeItemTypes.Add(BaseItemKind.Episode); } else { excludeItemTypes.Add(BaseItemKind.Episode); } } if (query.IsSports ?? false) { genres.Add("Sports"); } if (query.IsKids ?? false) { genres.Add("Kids"); genres.Add("Children"); genres.Add("Family"); } var limit = query.Limit; if (query.IsInProgress ?? false) { // limit = (query.Limit ?? 10) * 2; limit = null; // var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray(); // var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i is not null).ToArray(); // return new QueryResult // { // Items = items, // TotalRecordCount = items.Length // }; dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray(); } var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { MediaTypes = new[] { MediaType.Video }, Recursive = true, AncestorIds = folderIds, IsFolder = false, IsVirtualItem = false, Limit = limit, StartIndex = query.StartIndex, OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, EnableTotalRecordCount = query.EnableTotalRecordCount, IncludeItemTypes = includeItemTypes.ToArray(), ExcludeItemTypes = excludeItemTypes.ToArray(), Genres = genres.ToArray(), DtoOptions = dtoOptions }); if (query.IsInProgress ?? false) { // TODO: Fix The co-variant conversion between Video[] and BaseItem[], this can generate runtime issues. result.Items = result .Items .OfType