#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; 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.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv { /// /// Class LiveTvManager. /// public class LiveTvManager : ILiveTvManager { private const int MaxGuideDays = 14; private const string ExternalServiceTag = "ExternalServiceId"; private const string EtagKey = "ProgramEtag"; private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IItemRepository _itemRepo; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; private readonly IUserDataManager _userDataManager; private readonly ILibraryManager _libraryManager; private readonly ITaskManager _taskManager; private readonly ILocalizationManager _localization; private readonly IFileSystem _fileSystem; private readonly IChannelManager _channelManager; private readonly LiveTvDtoService _tvDtoService; private ILiveTvService[] _services = Array.Empty(); private ITunerHost[] _tunerHosts = Array.Empty(); private IListingsProvider[] _listingProviders = Array.Empty(); public LiveTvManager( IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IFileSystem fileSystem, IChannelManager channelManager, LiveTvDtoService liveTvDtoService) { _config = config; _logger = logger; _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; _taskManager = taskManager; _localization = localization; _fileSystem = fileSystem; _dtoService = dtoService; _userDataManager = userDataManager; _channelManager = channelManager; _tvDtoService = liveTvDtoService; } 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 ITunerHost[] TunerHosts => _tunerHosts; public IListingsProvider[] ListingProviders => _listingProviders; private LiveTvOptions GetConfiguration() { return _config.GetConfiguration("livetv"); } public string GetEmbyTvActiveRecordingPath(string id) { return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); } /// /// Adds the parts. /// /// The services. /// The tuner hosts. /// The listing providers. public void AddParts(IEnumerable services, IEnumerable tunerHosts, IEnumerable listingProviders) { _services = services.ToArray(); _tunerHosts = tunerHosts.Where(i => i.IsSupported).ToArray(); _listingProviders = listingProviders.ToArray(); foreach (var service in _services) { if (service is EmbyTV.EmbyTV embyTv) { embyTv.TimerCreated += OnEmbyTvTimerCreated; embyTv.TimerCancelled += OnEmbyTvTimerCancelled; } } } 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 List GetTunerHostTypes() { return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair { Name = i.Name, Id = i.Type }).ToList(); } public Task> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) { return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken); } 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.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))) { 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; ILiveStream liveStream; 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; } } } } private async Task GetChannelAsync(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; var isNew = false; var forceUpdate = false; var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id); var item = _libraryManager.GetItemById(id) as LiveTvChannel; if (item == null) { item = new LiveTvChannel { Name = channelInfo.Name, Id = id, DateCreated = DateTime.UtcNow }; isNew = true; } if (channelInfo.Tags != null) { if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase)) { isNew = true; } item.Tags = channelInfo.Tags; } if (!item.ParentId.Equals(parentFolderId)) { isNew = true; } item.ParentId = parentFolderId; item.ChannelType = channelInfo.ChannelType; item.ServiceName = serviceName; if (!string.Equals(item.GetProviderId(ExternalServiceTag), serviceName, StringComparison.OrdinalIgnoreCase)) { forceUpdate = true; } item.SetProviderId(ExternalServiceTag, serviceName); if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal)) { forceUpdate = true; } item.ExternalId = channelInfo.Id; if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal)) { forceUpdate = true; } item.Number = channelInfo.Number; if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal)) { forceUpdate = true; } item.Name = channelInfo.Name; if (!item.HasImage(ImageType.Primary)) { if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath)) { item.SetImagePath(ImageType.Primary, channelInfo.ImagePath); forceUpdate = true; } else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl)) { item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl); forceUpdate = true; } } if (isNew) { _libraryManager.CreateItem(item, parentFolder); } else if (forceUpdate) { await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } return item; } private (LiveTvProgram Item, bool IsNew, bool IsUpdated) GetProgram(ProgramInfo info, Dictionary allExistingPrograms, LiveTvChannel channel) { var id = _tvDtoService.GetInternalProgramId(info.Id); var isNew = false; var forceUpdate = false; if (!allExistingPrograms.TryGetValue(id, out LiveTvProgram item)) { isNew = true; item = new LiveTvProgram { Name = info.Name, Id = id, DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow }; if (!string.IsNullOrEmpty(info.Etag)) { item.SetProviderId(EtagKey, info.Etag); } } if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase)) { item.ShowId = info.ShowId; forceUpdate = true; } var seriesId = info.SeriesId; if (!item.ParentId.Equals(channel.Id)) { forceUpdate = true; } item.ParentId = channel.Id; item.Audio = info.Audio; item.ChannelId = channel.Id; item.CommunityRating ??= info.CommunityRating; if ((item.CommunityRating ?? 0).Equals(0)) { item.CommunityRating = null; } item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal)) { forceUpdate = true; } item.ExternalSeriesId = seriesId; var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle); if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle)) { item.SeriesName = info.Name; } var tags = new List(); if (info.IsLive) { tags.Add("Live"); } if (info.IsPremiere) { tags.Add("Premiere"); } if (info.IsNews) { tags.Add("News"); } if (info.IsSports) { tags.Add("Sports"); } if (info.IsKids) { tags.Add("Kids"); } if (info.IsRepeat) { tags.Add("Repeat"); } if (info.IsMovie) { tags.Add("Movie"); } if (isSeries) { tags.Add("Series"); } item.Tags = tags.ToArray(); item.Genres = info.Genres.ToArray(); if (info.IsHD ?? false) { item.Width = 1280; item.Height = 720; } item.IsMovie = info.IsMovie; item.IsRepeat = info.IsRepeat; if (item.IsSeries != isSeries) { forceUpdate = true; } item.IsSeries = isSeries; item.Name = info.Name; item.OfficialRating ??= info.OfficialRating; item.Overview ??= info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; item.ProviderIds = info.ProviderIds; foreach (var providerId in info.SeriesProviderIds) { info.ProviderIds["Series" + providerId.Key] = providerId.Value; } if (item.StartDate != info.StartDate) { forceUpdate = true; } item.StartDate = info.StartDate; if (item.EndDate != info.EndDate) { forceUpdate = true; } item.EndDate = info.EndDate; item.ProductionYear = info.ProductionYear; if (!isSeries || info.IsRepeat) { item.PremiereDate = info.OriginalAirDate; } item.IndexNumber = info.EpisodeNumber; item.ParentIndexNumber = info.SeasonNumber; if (!item.HasImage(ImageType.Primary)) { if (!string.IsNullOrWhiteSpace(info.ImagePath)) { item.SetImage( new ItemImageInfo { Path = info.ImagePath, Type = ImageType.Primary }, 0); } else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) { item.SetImage( new ItemImageInfo { Path = info.ImageUrl, Type = ImageType.Primary }, 0); } } if (!item.HasImage(ImageType.Thumb)) { if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl)) { item.SetImage( new ItemImageInfo { Path = info.ThumbImageUrl, Type = ImageType.Thumb }, 0); } } if (!item.HasImage(ImageType.Logo)) { if (!string.IsNullOrWhiteSpace(info.LogoImageUrl)) { item.SetImage( new ItemImageInfo { Path = info.LogoImageUrl, Type = ImageType.Logo }, 0); } } if (!item.HasImage(ImageType.Backdrop)) { if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl)) { item.SetImage( new ItemImageInfo { Path = info.BackdropImageUrl, Type = ImageType.Backdrop }, 0); } } var isUpdated = false; if (isNew) { } else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) { isUpdated = true; } else { var etag = info.Etag; if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase)) { item.SetProviderId(EtagKey, etag); isUpdated = true; } } if (isNew || isUpdated) { item.OnMetadataChanged(); } return (item, isNew, isUpdated); } 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 != 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 == 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 != 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 != null) { program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id) .ToString("N", CultureInfo.InvariantCulture); } } } internal Task RefreshChannels(IProgress progress, CancellationToken cancellationToken) { return RefreshChannelsInternal(progress, cancellationToken); } private async Task RefreshChannelsInternal(IProgress progress, CancellationToken cancellationToken) { await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false); await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false); var numComplete = 0; double progressPerService = _services.Length == 0 ? 0 : 1.0 / _services.Length; var newChannelIdList = new List(); var newProgramIdList = new List(); var cleanDatabase = true; foreach (var service in _services) { cancellationToken.ThrowIfCancellationRequested(); _logger.LogDebug("Refreshing guide from {Name}", service.Name); try { var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(p => progress.Report(p * progressPerService)); var idList = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(false); newChannelIdList.AddRange(idList.Item1); newProgramIdList.AddRange(idList.Item2); } catch (OperationCanceledException) { throw; } catch (Exception ex) { cleanDatabase = false; _logger.LogError(ex, "Error refreshing channels for service"); } numComplete++; double percent = numComplete; percent /= _services.Length; progress.Report(100 * percent); } if (cleanDatabase) { CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { BaseItemKind.LiveTvChannel }, progress, cancellationToken); CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { BaseItemKind.LiveTvProgram }, progress, cancellationToken); } var coreService = _services.OfType().FirstOrDefault(); if (coreService != null) { await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false); await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false); } // Load these now which will prefetch metadata var dtoOptions = new DtoOptions(); var fields = dtoOptions.Fields.ToList(); fields.Remove(ItemFields.BasicSyncInfo); dtoOptions.Fields = fields.ToArray(); progress.Report(100); } private async Task, List>> RefreshChannelsInternal(ILiveTvService service, IProgress progress, CancellationToken cancellationToken) { progress.Report(10); var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false)) .Select(i => new Tuple(service.Name, i)) .ToList(); var list = new List(); var numComplete = 0; var parentFolder = GetInternalLiveTvFolder(cancellationToken); foreach (var channelInfo in allChannelsList) { cancellationToken.ThrowIfCancellationRequested(); try { var item = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false); list.Add(item); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name); } numComplete++; double percent = numComplete; percent /= allChannelsList.Count; progress.Report((5 * percent) + 10); } progress.Report(15); numComplete = 0; var programs = new List(); var channels = new List(); var guideDays = GetGuideDays(); _logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays); cancellationToken.ThrowIfCancellationRequested(); foreach (var currentChannel in list) { channels.Add(currentChannel.Id); cancellationToken.ThrowIfCancellationRequested(); try { var start = DateTime.UtcNow.AddHours(-1); var end = start.AddDays(guideDays); var isMovie = false; var isSports = false; var isNews = false; var isKids = false; var iSSeries = false; var channelPrograms = (await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false)).ToList(); var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, ChannelIds = new Guid[] { currentChannel.Id }, DtoOptions = new DtoOptions(true) }).Cast().ToDictionary(i => i.Id); var newPrograms = new List(); var updatedPrograms = new List(); foreach (var program in channelPrograms) { var programTuple = GetProgram(program, existingPrograms, currentChannel); var programItem = programTuple.Item; if (programTuple.IsNew) { newPrograms.Add(programItem); } else if (programTuple.IsUpdated) { updatedPrograms.Add(programItem); } programs.Add(programItem.Id); isMovie |= program.IsMovie; iSSeries |= program.IsSeries; isSports |= program.IsSports; isNews |= program.IsNews; isKids |= program.IsKids; } _logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count); if (newPrograms.Count > 0) { _libraryManager.CreateItems(newPrograms, null, cancellationToken); } if (updatedPrograms.Count > 0) { await _libraryManager.UpdateItemsAsync( updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } currentChannel.IsMovie = isMovie; currentChannel.IsNews = isNews; currentChannel.IsSports = isSports; currentChannel.IsSeries = iSSeries; if (isKids) { currentChannel.AddTag("Kids"); } await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); await currentChannel.RefreshMetadata( new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name); } numComplete++; double percent = numComplete / (double)allChannelsList.Count; progress.Report((85 * percent) + 15); } progress.Report(100); return new Tuple, List>(channels, programs); } private void CleanDatabaseInternal(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress progress, CancellationToken cancellationToken) { var list = _itemRepo.GetItemIdsList(new InternalItemsQuery { IncludeItemTypes = validTypes, DtoOptions = new DtoOptions(false) }); var numComplete = 0; foreach (var itemId in list) { cancellationToken.ThrowIfCancellationRequested(); if (itemId.Equals(default)) { // Somehow some invalid data got into the db. It probably predates the boundary checking continue; } if (!currentIdList.Contains(itemId)) { var item = _libraryManager.GetItemById(itemId); if (item != null) { _libraryManager.DeleteItem( item, new DeleteOptions { DeleteFileLocation = false, DeleteFromExternalProvider = false }, false); } } numComplete++; double percent = numComplete / (double)list.Count; progress.Report(100 * percent); } } private double GetGuideDays() { var config = GetConfiguration(); if (config.GuideDays.HasValue) { return Math.Max(1, Math.Min(config.GuideDays.Value, MaxGuideDays)); } return 7; } private QueryResult GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user) { if (user == null) { return new QueryResult(); } var folderIds = GetRecordingFolders(user, true) .Select(i => i.Id) .ToList(); var excludeItemTypes = new List(); if (folderIds.Count == 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 != 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.ToArray(), 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