diff options
Diffstat (limited to 'Emby.Server.Implementations/LiveTv/LiveTvManager.cs')
| -rw-r--r-- | Emby.Server.Implementations/LiveTv/LiveTvManager.cs | 2408 |
1 files changed, 0 insertions, 2408 deletions
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs deleted file mode 100644 index 426165de61..0000000000 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ /dev/null @@ -1,2408 +0,0 @@ -#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 -{ - /// <summary> - /// Class LiveTvManager. - /// </summary> - 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<LiveTvManager> _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<ILiveTvService>(); - private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); - private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); - - public LiveTvManager( - IServerConfigurationManager config, - ILogger<LiveTvManager> 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<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; - - public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; - - public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; - - public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; - - /// <summary> - /// Gets the services. - /// </summary> - /// <value>The services.</value> - public IReadOnlyList<ILiveTvService> Services => _services; - - public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts; - - public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders; - - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration<LiveTvOptions>("livetv"); - } - - public string GetEmbyTvActiveRecordingPath(string id) - { - return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); - } - - /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="services">The services.</param> - /// <param name="tunerHosts">The tuner hosts.</param> - /// <param name="listingProviders">The listing providers.</param> - public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders) - { - _services = services.ToArray(); - _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<string> e) - { - var timerId = e.Argument; - - TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(timerId))); - } - - private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e) - { - var timer = e.Argument; - - TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( - new TimerEventInfo(timer.Id) - { - ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId) - })); - } - - public List<NameIdPair> GetTunerHostTypes() - { - return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair - { - Name = i.Name, - Id = i.Type - }).ToList(); - } - - public Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) - { - return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken); - } - - public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) - { - 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<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> 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<Task> 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<MediaSourceInfo, ILiveStream>(info, liveStream); - } - - public async Task<IEnumerable<MediaSourceInfo>> 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<LiveTvChannel> 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 is null) - { - item = new LiveTvChannel - { - Name = channelInfo.Name, - Id = id, - DateCreated = DateTime.UtcNow - }; - - isNew = true; - } - - if (channelInfo.Tags is not 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<Guid, LiveTvProgram> 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<string>(); - 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<BaseItemDto> 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<QueryResult<BaseItemDto>> 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<BaseItemDto>(); - } - } - else - { - // Better to return nothing than every program in the database - return new QueryResult<BaseItemDto>(); - } - } - - var queryResult = _libraryManager.QueryItems(internalQuery); - - var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); - - return new QueryResult<BaseItemDto>( - query.StartIndex, - queryResult.TotalRecordCount, - returnArray); - } - - public QueryResult<BaseItem> 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<LiveTvProgram>().OrderBy(i => i.StartDate.Date); - - if (query.IsAiring ?? false) - { - orderedPrograms = orderedPrograms - .ThenByDescending(i => GetRecommendationScore(i, user, true)); - } - - IEnumerable<BaseItem> programs = orderedPrograms; - - if (query.Limit.HasValue) - { - programs = programs.Take(query.Limit.Value); - } - - return new QueryResult<BaseItem>( - query.StartIndex, - totalCount, - programs.ToArray()); - } - - public Task<QueryResult<BaseItemDto>> 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<BaseItemDto>( - 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<TimerInfo> timerList = null; - IReadOnlyList<SeriesTimerInfo> 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); - } - } - } - - internal Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken) - { - return RefreshChannelsInternal(progress, cancellationToken); - } - - private async Task RefreshChannelsInternal(IProgress<double> 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<Guid>(); - var newProgramIdList = new List<Guid>(); - - var cleanDatabase = true; - - foreach (var service in _services) - { - cancellationToken.ThrowIfCancellationRequested(); - - _logger.LogDebug("Refreshing guide from {Name}", service.Name); - - try - { - var innerProgress = new ActionableProgress<double>(); - 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<EmbyTV.EmbyTV>().FirstOrDefault(); - - if (coreService is not 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(); - dtoOptions.Fields = fields.ToArray(); - - progress.Report(100); - } - - private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, ActionableProgress<double> progress, CancellationToken cancellationToken) - { - progress.Report(10); - - var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false)) - .Select(i => new Tuple<string, ChannelInfo>(service.Name, i)) - .ToList(); - - var list = new List<LiveTvChannel>(); - - 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<Guid>(); - var channels = new List<Guid>(); - - 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<LiveTvProgram>().ToDictionary(i => i.Id); - - var newPrograms = new List<LiveTvProgram>(); - var updatedPrograms = new List<BaseItem>(); - - 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<Guid>, List<Guid>>(channels, programs); - } - - private void CleanDatabaseInternal(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> 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 is not 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 async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user) - { - if (user is null) - { - return new QueryResult<BaseItem>(); - } - - var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false); - var folderIds = Array.ConvertAll(folders, x => x.Id); - - var excludeItemTypes = new List<BaseItemKind>(); - - if (folderIds.Length == 0) - { - return new QueryResult<BaseItem>(); - } - - var includeItemTypes = new List<BaseItemKind>(); - var genres = new List<string>(); - - 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<BaseItem> - // { - // 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<Video>() - .Where(i => !i.IsCompleteMedia) - .ToArray(); - - result.TotalRecordCount = result.Items.Count; - } - - return result; - } - - public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null) - { - var programTuples = new List<(BaseItemDto Dto, string ExternalId, string ExternalSeriesId)>(); - var hasChannelImage = fields.Contains(ItemFields.ChannelImage); - var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo); - - foreach (var (item, dto) in programs) - { - var program = (LiveTvProgram)item; - - dto.StartDate = program.StartDate; - dto.EpisodeTitle = program.EpisodeTitle; - dto.IsRepeat |= program.IsRepeat; - dto.IsMovie |= program.IsMovie; - dto.IsSeries |= program.IsSeries; - dto.IsSports |= program.IsSports; - dto.IsLive |= program.IsLive; - dto.IsNews |= program.IsNews; - dto.IsKids |= program.IsKids; - dto.IsPremiere |= program.IsPremiere; - - if (hasChannelInfo || hasChannelImage) - { - var channel = _libraryManager.GetItemById(program.ChannelId); - - if (channel is LiveTvChannel liveChannel) - { - dto.ChannelName = liveChannel.Name; - dto.MediaType = liveChannel.MediaType; - dto.ChannelNumber = liveChannel.Number; - - if (hasChannelImage && liveChannel.HasImage(ImageType.Primary)) - { - dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(liveChannel); - } - } - } - - programTuples.Add((dto, program.ExternalId, program.ExternalSeriesId)); - } - - return AddRecordingInfo(programTuples, CancellationToken.None); - } - - public ActiveRecordingInfo GetActiveRecordingInfo(string path) - { - return EmbyTV.EmbyTV.Current.GetActiveRecordingInfo(path); - } - - public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null) - { - var service = EmbyTV.EmbyTV.Current; - - var info = activeRecordingInfo.Timer; - - var channel = string.IsNullOrWhiteSpace(info.ChannelId) ? null : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(service.Name, info.ChannelId)); - - dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) - ? null - : _tvDtoService.GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N", CultureInfo.InvariantCulture); - - dto.TimerId = string.IsNullOrEmpty(info.Id) - ? null - : _tvDtoService.GetInternalTimerId(info.Id); - - var startDate = info.StartDate; - var endDate = info.EndDate; - - dto.StartDate = startDate; - dto.EndDate = endDate; - dto.Status = info.Status.ToString(); - dto.IsRepeat = info.IsRepeat; - dto.EpisodeTitle = info.EpisodeTitle; - dto.IsMovie = info.IsMovie; - dto.IsSeries = info.IsSeries; - dto.IsSports = info.IsSports; - dto.IsLive = info.IsLive; - dto.IsNews = info.IsNews; - dto.IsKids = info.IsKids; - dto.IsPremiere = info.IsPremiere; - - if (info.Status == RecordingStatus.InProgress) - { - startDate = info.StartDate.AddSeconds(0 - info.PrePaddingSeconds); - endDate = info.EndDate.AddSeconds(info.PostPaddingSeconds); - - var now = DateTime.UtcNow.Ticks; - var start = startDate.Ticks; - var end = endDate.Ticks; - - var pct = now - start; - - pct /= end; - pct *= 100; - dto.CompletionPercentage = pct; - } - - if (channel is not null) - { - dto.ChannelName = channel.Name; - - if (channel.HasImage(ImageType.Primary)) - { - dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); - } - } - } - - public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options) - { - var user = query.UserId.Equals(default) - ? null - : _userManager.GetUserById(query.UserId); - - RemoveFields(options); - - var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false); - - var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); - - return new QueryResult<BaseItemDto>( - query.StartIndex, - internalResult.TotalRecordCount, - returnArray); - } - - private async Task<QueryResult<TimerInfo>> GetTimersInternal(TimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => new Tuple<TimerInfo, ILiveTvService>(r, i)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<TimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (query.IsActive.HasValue) - { - if (query.IsActive.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress); - } - } - - if (query.IsScheduled.HasValue) - { - if (query.IsScheduled.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.New); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.New); - } - } - - if (!string.IsNullOrEmpty(query.ChannelId)) - { - var guid = new Guid(query.ChannelId); - timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.SeriesTimerId)) - { - var guid = new Guid(query.SeriesTimerId); - - timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.Id)) - { - timers = timers - .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); - } - - var returnArray = timers - .Select(i => i.Item1) - .OrderBy(i => i.StartDate) - .ToArray(); - - return new QueryResult<TimerInfo>(returnArray); - } - - public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => new Tuple<TimerInfo, ILiveTvService>(r, i)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<TimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (query.IsActive.HasValue) - { - if (query.IsActive.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress); - } - } - - if (query.IsScheduled.HasValue) - { - if (query.IsScheduled.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.New); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.New); - } - } - - if (!string.IsNullOrEmpty(query.ChannelId)) - { - var guid = new Guid(query.ChannelId); - timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.SeriesTimerId)) - { - var guid = new Guid(query.SeriesTimerId); - - timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.Id)) - { - timers = timers - .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); - } - - var returnList = new List<TimerInfoDto>(); - - foreach (var i in timers) - { - var program = string.IsNullOrEmpty(i.Item1.ProgramId) ? - null : - _libraryManager.GetItemById(_tvDtoService.GetInternalProgramId(i.Item1.ProgramId)) as LiveTvProgram; - - var channel = string.IsNullOrEmpty(i.Item1.ChannelId) ? null : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); - - returnList.Add(_tvDtoService.GetTimerInfoDto(i.Item1, i.Item2, program, channel)); - } - - var returnArray = returnList - .OrderBy(i => i.StartDate) - .ToArray(); - - return new QueryResult<TimerInfoDto>(returnArray); - } - - public async Task CancelTimer(string id) - { - var timer = await GetTimer(id, CancellationToken.None).ConfigureAwait(false); - - if (timer is null) - { - throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id)); - } - - var service = GetService(timer.ServiceName); - - await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - - if (service is not EmbyTV.EmbyTV) - { - TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id))); - } - } - - public async Task CancelSeriesTimer(string id) - { - var timer = await GetSeriesTimer(id, CancellationToken.None).ConfigureAwait(false); - - if (timer is null) - { - throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id)); - } - - var service = GetService(timer.ServiceName); - - await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - - SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id))); - } - - public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) - { - var results = await GetTimers( - new TimerQuery - { - Id = id - }, - cancellationToken).ConfigureAwait(false); - - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); - } - - public async Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken) - { - var results = await GetSeriesTimers(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); - - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); - } - - private async Task<QueryResult<SeriesTimerInfo>> GetSeriesTimersInternal(SeriesTimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => - { - r.ServiceName = i.Name; - return new Tuple<SeriesTimerInfo, ILiveTvService>(r, i); - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<SeriesTimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase)) - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderBy(i => i.Item1.Priority).ThenByStringDescending(i => i.Item1.Name) : - timers.OrderByDescending(i => i.Item1.Priority).ThenByString(i => i.Item1.Name); - } - else - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderByStringDescending(i => i.Item1.Name) : - timers.OrderByString(i => i.Item1.Name); - } - - var returnArray = timers - .Select(i => i.Item1) - .ToArray(); - - return new QueryResult<SeriesTimerInfo>(returnArray); - } - - public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => new Tuple<SeriesTimerInfo, ILiveTvService>(r, i)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<SeriesTimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase)) - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderBy(i => i.Item1.Priority).ThenByStringDescending(i => i.Item1.Name) : - timers.OrderByDescending(i => i.Item1.Priority).ThenByString(i => i.Item1.Name); - } - else - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderByStringDescending(i => i.Item1.Name) : - timers.OrderByString(i => i.Item1.Name); - } - - var returnArray = timers - .Select(i => - { - string channelName = null; - - if (!string.IsNullOrEmpty(i.Item1.ChannelId)) - { - var internalChannelId = _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId); - var channel = _libraryManager.GetItemById(internalChannelId); - channelName = channel is null ? null : channel.Name; - } - - return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName); - }) - .ToArray(); - - return new QueryResult<SeriesTimerInfoDto>(returnArray); - } - - public BaseItem GetLiveTvChannel(TimerInfo timer, ILiveTvService service) - { - var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, timer.ChannelId); - return _libraryManager.GetItemById(internalChannelId); - } - - public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto ItemDto, LiveTvChannel Channel)> items, DtoOptions options, User user) - { - var now = DateTime.UtcNow; - - var channelIds = items.Select(i => i.Channel.Id).Distinct().ToArray(); - - var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, - ChannelIds = channelIds, - MaxStartDate = now, - MinEndDate = now, - Limit = channelIds.Length, - OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id }, - DtoOptions = options - }) : new List<BaseItem>(); - - RemoveFields(options); - - var currentProgramsList = new List<BaseItem>(); - var currentChannelsDict = new Dictionary<Guid, BaseItemDto>(); - - var addCurrentProgram = options.AddCurrentProgram; - - foreach (var (dto, channel) in items) - { - dto.Number = channel.Number; - dto.ChannelNumber = channel.Number; - dto.ChannelType = channel.ChannelType; - - currentChannelsDict[dto.Id] = dto; - - if (addCurrentProgram) - { - var currentProgram = programs.FirstOrDefault(i => channel.Id.Equals(i.ChannelId)); - - if (currentProgram is not null) - { - currentProgramsList.Add(currentProgram); - } - } - } - - if (addCurrentProgram) - { - var currentProgramDtos = _dtoService.GetBaseItemDtos(currentProgramsList, options, user); - - foreach (var programDto in currentProgramDtos) - { - if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto)) - { - channelDto.CurrentProgram = programDto; - } - } - } - } - - private async Task<Tuple<SeriesTimerInfo, ILiveTvService>> GetNewTimerDefaultsInternal(CancellationToken cancellationToken, LiveTvProgram program = null) - { - ILiveTvService service = null; - ProgramInfo programInfo = null; - - if (program is not null) - { - service = GetService(program); - - var channel = _libraryManager.GetItemById(program.ChannelId); - - programInfo = new ProgramInfo - { - Audio = program.Audio, - ChannelId = channel.ExternalId, - CommunityRating = program.CommunityRating, - EndDate = program.EndDate ?? DateTime.MinValue, - EpisodeTitle = program.EpisodeTitle, - Genres = program.Genres.ToList(), - Id = program.ExternalId, - IsHD = program.IsHD, - IsKids = program.IsKids, - IsLive = program.IsLive, - IsMovie = program.IsMovie, - IsNews = program.IsNews, - IsPremiere = program.IsPremiere, - IsRepeat = program.IsRepeat, - IsSeries = program.IsSeries, - IsSports = program.IsSports, - OriginalAirDate = program.PremiereDate, - Overview = program.Overview, - StartDate = program.StartDate, - // ImagePath = program.ExternalImagePath, - Name = program.Name, - OfficialRating = program.OfficialRating - }; - } - - service ??= _services[0]; - - var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); - - info.RecordAnyTime = true; - info.Days = new List<DayOfWeek> - { - DayOfWeek.Sunday, - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday, - DayOfWeek.Saturday - }; - - info.Id = null; - - return new Tuple<SeriesTimerInfo, ILiveTvService>(info, service); - } - - public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken) - { - var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false); - - return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); - } - - public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) - { - var program = (LiveTvProgram)_libraryManager.GetItemById(programId); - var programDto = await GetProgram(programId, cancellationToken).ConfigureAwait(false); - - var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); - var info = _tvDtoService.GetSeriesTimerInfoDto(defaults.Item1, defaults.Item2, null); - - info.Days = defaults.Item1.Days.ToArray(); - - info.DayPattern = _tvDtoService.GetDayPattern(info.Days); - - info.Name = program.Name; - info.ChannelId = programDto.ChannelId ?? Guid.Empty; - info.ChannelName = programDto.ChannelName; - info.StartDate = program.StartDate; - info.Name = program.Name; - info.Overview = program.Overview; - info.ProgramId = programDto.Id.ToString("N", CultureInfo.InvariantCulture); - info.ExternalProgramId = program.ExternalId; - - if (program.EndDate.HasValue) - { - info.EndDate = program.EndDate.Value; - } - - return info; - } - - public async Task CreateTimer(TimerInfoDto timer, CancellationToken cancellationToken) - { - var service = GetService(timer.ServiceName); - - var info = await _tvDtoService.GetTimerInfo(timer, true, this, cancellationToken).ConfigureAwait(false); - - // Set priority from default values - var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); - info.Priority = defaultValues.Priority; - - string newTimerId = null; - if (service is ISupportsNewTimerIds supportsNewTimerIds) - { - newTimerId = await supportsNewTimerIds.CreateTimer(info, cancellationToken).ConfigureAwait(false); - newTimerId = _tvDtoService.GetInternalTimerId(newTimerId); - } - else - { - await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - _logger.LogInformation("New recording scheduled"); - - if (service is not EmbyTV.EmbyTV) - { - TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( - new TimerEventInfo(newTimerId) - { - ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId) - })); - } - } - - public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) - { - var service = GetService(timer.ServiceName); - - var info = await _tvDtoService.GetSeriesTimerInfo(timer, true, this, cancellationToken).ConfigureAwait(false); - - // Set priority from default values - var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); - info.Priority = defaultValues.Priority; - - string newTimerId = null; - if (service is ISupportsNewTimerIds supportsNewTimerIds) - { - newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false); - newTimerId = _tvDtoService.GetInternalSeriesTimerId(newTimerId).ToString("N", CultureInfo.InvariantCulture); - } - else - { - await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( - new TimerEventInfo(newTimerId) - { - ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId) - })); - } - - public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken) - { - var info = await _tvDtoService.GetTimerInfo(timer, false, this, cancellationToken).ConfigureAwait(false); - - var service = GetService(timer.ServiceName); - - await service.UpdateTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) - { - var info = await _tvDtoService.GetSeriesTimerInfo(timer, false, this, cancellationToken).ConfigureAwait(false); - - var service = GetService(timer.ServiceName); - - await service.UpdateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - public GuideInfo GetGuideInfo() - { - var startDate = DateTime.UtcNow; - var endDate = startDate.AddDays(GetGuideDays()); - - return new GuideInfo - { - StartDate = startDate, - EndDate = endDate - }; - } - - private LiveTvServiceInfo[] GetServiceInfos() - { - return Services.Select(GetServiceInfo).ToArray(); - } - - private static LiveTvServiceInfo GetServiceInfo(ILiveTvService service) - { - return new LiveTvServiceInfo - { - Name = service.Name - }; - } - - public LiveTvInfo GetLiveTvInfo(CancellationToken cancellationToken) - { - var services = GetServiceInfos(); - - var info = new LiveTvInfo - { - Services = services, - IsEnabled = services.Length > 0, - EnabledUsers = _userManager.Users - .Where(IsLiveTvEnabled) - .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) - .ToArray() - }; - - return info; - } - - private bool IsLiveTvEnabled(User user) - { - return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0); - } - - public IEnumerable<User> GetEnabledUsers() - { - return _userManager.Users - .Where(IsLiveTvEnabled); - } - - /// <summary> - /// Resets the tuner. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task ResetTuner(string id, CancellationToken cancellationToken) - { - var parts = id.Split('_', 2); - - var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase)); - - if (service is null) - { - throw new ArgumentException("Service not found."); - } - - return service.ResetTuner(parts[1], cancellationToken); - } - - private static void RemoveFields(DtoOptions options) - { - var fields = options.Fields.ToList(); - - fields.Remove(ItemFields.CanDelete); - fields.Remove(ItemFields.CanDownload); - fields.Remove(ItemFields.DisplayPreferencesId); - fields.Remove(ItemFields.Etag); - options.Fields = fields.ToArray(); - } - - public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken) - { - var name = _localization.GetLocalizedString("HeaderLiveTV"); - return _libraryManager.GetNamedView(name, CollectionType.livetv, name); - } - - public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) - { - info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info)); - - var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ResourceNotFoundException(); - } - - if (provider is IConfigurableTunerHost configurable) - { - await configurable.Validate(info).ConfigureAwait(false); - } - - var config = GetConfiguration(); - - var list = config.TunerHosts.ToList(); - var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); - - if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) - { - info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - list.Add(info); - config.TunerHosts = list.ToArray(); - } - else - { - config.TunerHosts[index] = info; - } - - _config.SaveConfiguration("livetv", config); - - if (dataSourceChanged) - { - _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); - } - - return info; - } - - public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings) - { - // 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); - - LiveTvOptions config = GetConfiguration(); - - 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 = GetConfiguration(); - - 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 = GetConfiguration(); - - 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 = GetConfiguration(); - - 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 = GetConfiguration().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 = GetConfiguration().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); - } - - public Guid GetInternalChannelId(string serviceName, string externalId) - { - return _tvDtoService.GetInternalChannelId(serviceName, externalId); - } - - public Guid GetInternalProgramId(string externalId) - { - return _tvDtoService.GetInternalProgramId(externalId); - } - - /// <inheritdoc /> - public Task<BaseItem[]> GetRecordingFoldersAsync(User user) - => GetRecordingFoldersAsync(user, false); - - private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels) - { - var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() - .SelectMany(i => i.Locations) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i is not null && i.IsVisibleStandalone(user)) - .SelectMany(i => _libraryManager.GetCollectionFolders(i)) - .DistinctBy(x => x.Id) - .OrderBy(i => i.SortName) - .ToList(); - - var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery - { - UserId = user.Id, - IsRecordingsFolder = true, - RefreshLatestChannelItems = refreshChannels - }).ConfigureAwait(false); - - folders.AddRange(channels.Items); - - return folders.Cast<BaseItem>().ToArray(); - } - } -} |
