#pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; 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.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv { /// /// Class LiveTvManager. /// public class LiveTvManager : ILiveTvManager, IDisposable { 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 IJsonSerializer _jsonSerializer; 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, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IChannelManager channelManager, LiveTvDtoService liveTvDtoService) { _config = config; _logger = logger; _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; _taskManager = taskManager; _localization = localization; _jsonSerializer = jsonSerializer; _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 { Argument = new TimerEventInfo { Id = timerId } }); } private void OnEmbyTvTimerCreated(object sender, GenericEventArgs e) { var timer = e.Argument; TimerCreated?.Invoke(this, new GenericEventArgs { Argument = new TimerEventInfo { ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId), Id = timer.Id } }); } 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 == Guid.Empty ? 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[] { typeof(LiveTvChannel).Name }, 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.Item1, 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.AddRange(new List { 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.AddRange(new List { 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 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 const string ExternalServiceTag = "ExternalServiceId"; private LiveTvChannel GetChannel(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) { _libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken); } return item; } private const string EtagKey = "ProgramEtag"; private Tuple GetProgram(ProgramInfo info, Dictionary allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) { 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.ChannelType = channelType; item.Audio = info.Audio; item.ChannelId = channel.Id; item.CommunityRating = 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 = item.OfficialRating ?? info.OfficialRating; item.Overview = 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 new Tuple(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>() { new Tuple(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) { if (query.IsAiring ?? false) { // Unless something else was specified, order by start date to take advantage of a specialized index query.OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }; } else { // 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[] { typeof(LiveTvProgram).Name }, 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); var result = new QueryResult { Items = returnArray, TotalRecordCount = queryResult.TotalRecordCount }; return result; } public QueryResult GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { var user = query.User; var topFolder = GetInternalLiveTvFolder(cancellationToken); var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, 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 { Items = programs.ToArray(), TotalRecordCount = totalCount }; } public QueryResult GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { if (!(query.IsAiring ?? false)) { return GetPrograms(query, options, cancellationToken).Result; } RemoveFields(options); var internalResult = GetRecommendedProgramsInternal(query, options, cancellationToken); return new QueryResult { Items = _dtoService.GetBaseItemDtos(internalResult.Items, options, query.User), TotalRecordCount = internalResult.TotalRecordCount }; } 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> programs, CancellationToken cancellationToken) { IReadOnlyList timerList = null; IReadOnlyList seriesTimerList = null; foreach (var programTuple in programs) { var program = programTuple.Item1; var externalProgramId = programTuple.Item2; string externalSeriesId = programTuple.Item3; if (timerList == null) { 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; } if (seriesTimerList == null) { 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[] { typeof(LiveTvChannel).Name }, progress, cancellationToken); CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, 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 = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken); 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 string[] { typeof(LiveTvProgram).Name }, 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, currentChannel.ChannelType, service.Name, cancellationToken); var programItem = programTuple.Item1; if (programTuple.Item2) { newPrograms.Add(programItem); } else if (programTuple.Item3) { 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) { _libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken); } currentChannel.IsMovie = isMovie; currentChannel.IsNews = isNews; currentChannel.IsSports = isSports; currentChannel.IsSeries = iSSeries; if (isKids) { currentChannel.AddTag("Kids"); } currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); 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, string[] 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(Guid.Empty)) { // 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 const int MaxGuideDays = 14; 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(typeof(Movie).Name); } else { excludeItemTypes.Add(typeof(Movie).Name); } } if (query.IsSeries.HasValue) { if (query.IsSeries.Value) { includeItemTypes.Add(typeof(Episode).Name); } else { excludeItemTypes.Add(typeof(Episode).Name); } } 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