From afd94407f9bad2030344e93c1ce04008209a65af Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 23 Aug 2017 15:45:52 -0400 Subject: [PATCH] rework active recordings --- Emby.Server.Implementations/Dto/DtoService.cs | 15 ++ .../LiveTv/EmbyTV/EmbyTV.cs | 108 +++++++++++++-- .../LiveTv/EmbyTV/RecordingHelper.cs | 4 + .../LiveTv/LiveTvManager.cs | 129 ++++++++++++------ .../LiveTv/LiveTvMediaSourceProvider.cs | 22 ++- .../Entities/IHasMediaSources.cs | 2 +- MediaBrowser.Controller/Entities/Video.cs | 39 +++++- .../LiveTv/ILiveTvManager.cs | 4 + .../LiveTv/ILiveTvRecording.cs | 9 ++ MediaBrowser.Controller/LiveTv/TimerInfo.cs | 3 + 10 files changed, 270 insertions(+), 65 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2ea88b52a4..17e91bfa88 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -411,6 +411,21 @@ namespace Emby.Server.Implementations.Dto { liveTvManager.AddInfoToRecordingDto(item, dto, user); } + else + { + var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); + if (activeRecording != null) + { + dto.Type = "Recording"; + dto.CanDownload = false; + if (!string.IsNullOrWhiteSpace(dto.SeriesName)) + { + dto.EpisodeTitle = dto.Name; + dto.Name = dto.SeriesName; + } + liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user); + } + } return dto; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index ecad5c0eb2..f3d40ae19e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -830,6 +830,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV existingTimer.IsKids = updatedTimer.IsKids; existingTimer.IsNews = updatedTimer.IsNews; existingTimer.IsMovie = updatedTimer.IsMovie; + existingTimer.IsSeries = updatedTimer.IsSeries; + existingTimer.IsLive = updatedTimer.IsLive; + existingTimer.IsPremiere = updatedTimer.IsPremiere; existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries; existingTimer.IsRepeat = updatedTimer.IsRepeat; existingTimer.IsSports = updatedTimer.IsSports; @@ -861,7 +864,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task> GetRecordingsAsync(CancellationToken cancellationToken) { - return _activeRecordings.Values.ToList().Select(GetRecordingInfo).ToList(); + return new List(); + //return _activeRecordings.Values.ToList().Select(GetRecordingInfo).ToList(); } public string GetActiveRecordingPath(string id) @@ -875,6 +879,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } + public IEnumerable GetAllActiveRecordings() + { + return _activeRecordings.Values; + } + + public ActiveRecordingInfo GetActiveRecordingInfo(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + foreach (var recording in _activeRecordings.Values) + { + if (string.Equals(recording.Path, path, StringComparison.Ordinal)) + { + return recording; + } + } + return null; + } + private RecordingInfo GetRecordingInfo(ActiveRecordingInfo info) { var timer = info.Timer; @@ -1245,6 +1271,33 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new FileNotFoundException(); } + public async Task> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) + { + var stream = new MediaSourceInfo + { + Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + Id = info.Id, + SupportsDirectPlay = false, + SupportsDirectStream = true, + SupportsTranscoding = true, + IsInfiniteStream = true, + RequiresOpening = false, + RequiresClosing = false, + Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http, + BufferMs = 0, + IgnoreDts = true, + IgnoreIndex = true + }; + + var isAudio = false; + await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false); + + return new List + { + stream + }; + } + public async Task CloseLiveStream(string id, CancellationToken cancellationToken) { // Ignore the consumer id @@ -1327,7 +1380,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var activeRecordingInfo = new ActiveRecordingInfo { CancellationTokenSource = new CancellationTokenSource(), - Timer = timer + Timer = timer, + Id = timer.Id }; if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo)) @@ -1493,7 +1547,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath); recordPath = EnsureFileUnique(recordPath, timer.Id); - _libraryManager.RegisterIgnoredPath(recordPath); _libraryMonitor.ReportFileSystemChangeBeginning(recordPath); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(recordPath)); activeRecordingInfo.Path = recordPath; @@ -1512,6 +1565,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _timerProvider.AddOrUpdate(timer, false); SaveRecordingMetadata(timer, recordPath, seriesPath); + TriggerRefresh(recordPath); EnforceKeepUpTo(timer, seriesPath); }; @@ -1543,7 +1597,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - _libraryManager.UnRegisterIgnoredPath(recordPath); + TriggerRefresh(recordPath); _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); ActiveRecordingInfo removed; @@ -1574,6 +1628,44 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV OnRecordingStatusChanged(); } + private void TriggerRefresh(string path) + { + var item = GetAffectedBaseItem(_fileSystem.GetDirectoryName(path)); + + if (item != null) + { + item.ChangedExternally(); + } + } + + private BaseItem GetAffectedBaseItem(string path) + { + BaseItem item = null; + + while (item == null && !string.IsNullOrEmpty(path)) + { + item = _libraryManager.FindByPath(path, null); + + path = _fileSystem.GetDirectoryName(path); + } + + if (item != null) + { + // If the item has been deleted find the first valid parent that still exists + while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) + { + item = item.GetParent(); + + if (item == null) + { + break; + } + } + } + + return item; + } + private void OnRecordingStatusChanged() { EventHelper.FireEventIfNotNull(RecordingStatusChanged, this, new RecordingStatusChangedEventArgs @@ -2621,14 +2713,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return list; } - class ActiveRecordingInfo - { - public string Path { get; set; } - public TimerInfo Timer { get; set; } - public ProgramInfo Program { get; set; } - public CancellationTokenSource CancellationTokenSource { get; set; } - } - private const int TunerDiscoveryDurationMs = 3000; public async Task> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 94be4a02ef..b5de6ef01e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -58,6 +58,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV timerInfo.OriginalAirDate = programInfo.OriginalAirDate; timerInfo.IsProgramSeries = programInfo.IsSeries; + timerInfo.IsSeries = programInfo.IsSeries; + timerInfo.IsLive = programInfo.IsLive; + timerInfo.IsPremiere = programInfo.IsPremiere; + timerInfo.HomePageUrl = programInfo.HomePageUrl; timerInfo.CommunityRating = programInfo.CommunityRating; timerInfo.Overview = programInfo.Overview; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 2882af007f..bf30546aba 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.LiveTv private readonly LiveTvDtoService _tvDtoService; - private readonly List _services = new List(); + private ILiveTvService[] _services = new ILiveTvService[] { }; private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1); @@ -124,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv /// The listing providers. public void AddParts(IEnumerable services, IEnumerable tunerHosts, IEnumerable listingProviders) { - _services.AddRange(services); + _services = services.ToArray(); _tunerHosts.AddRange(tunerHosts); _listingProviders.AddRange(listingProviders); @@ -1221,9 +1221,9 @@ namespace Emby.Server.Implementations.LiveTv await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false); var numComplete = 0; - double progressPerService = _services.Count == 0 + double progressPerService = _services.Length == 0 ? 0 - : 1 / _services.Count; + : 1 / _services.Length; var newChannelIdList = new List(); var newProgramIdList = new List(); @@ -1255,7 +1255,7 @@ namespace Emby.Server.Implementations.LiveTv numComplete++; double percent = numComplete; - percent /= _services.Count; + percent /= _services.Length; progress.Report(100 * percent); } @@ -1561,11 +1561,6 @@ namespace Emby.Server.Implementations.LiveTv return new QueryResult(); } - if ((query.IsInProgress ?? false)) - { - return new QueryResult(); - } - var folderIds = EmbyTV.EmbyTV.Current.GetRecordingFolders() .SelectMany(i => i.Locations) .Distinct(StringComparer.OrdinalIgnoreCase) @@ -1577,13 +1572,10 @@ namespace Emby.Server.Implementations.LiveTv var excludeItemTypes = new List(); - if (!query.IsInProgress.HasValue) - { - folderIds.Add(internalLiveTvFolderId); + folderIds.Add(internalLiveTvFolderId); - excludeItemTypes.Add(typeof(LiveTvChannel).Name); - excludeItemTypes.Add(typeof(LiveTvProgram).Name); - } + excludeItemTypes.Add(typeof(LiveTvChannel).Name); + excludeItemTypes.Add(typeof(LiveTvProgram).Name); if (folderIds.Count == 0) { @@ -1632,6 +1624,19 @@ namespace Emby.Server.Implementations.LiveTv } } + if ((query.IsInProgress ?? false)) + { + // TODO: filter + 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 + }; + } + return _libraryManager.GetItemsResult(new InternalItemsQuery(user) { MediaTypes = new[] { MediaType.Video }, @@ -1659,11 +1664,6 @@ namespace Emby.Server.Implementations.LiveTv return new QueryResult(); } - if (_services.Count > 1) - { - return new QueryResult(); - } - if (user == null || (query.IsInProgress ?? false)) { return new QueryResult(); @@ -1722,13 +1722,9 @@ namespace Emby.Server.Implementations.LiveTv var folder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); - if (_services.Count == 1 && (!query.IsInProgress.HasValue || !query.IsInProgress.Value) && (!query.IsLibraryItem.HasValue || query.IsLibraryItem.Value)) + // TODO: Figure out how to merge emby recordings + service recordings + if (_services.Length == 1) { - if (!query.IsInProgress.HasValue) - { - await RefreshRecordings(folder.Id, cancellationToken).ConfigureAwait(false); - } - return GetEmbyRecordings(query, options, folder.Id, user); } @@ -1920,6 +1916,11 @@ namespace Emby.Server.Implementations.LiveTv await AddRecordingInfo(programTuples, CancellationToken.None).ConfigureAwait(false); } + public ActiveRecordingInfo GetActiveRecordingInfo(string path) + { + return EmbyTV.EmbyTV.Current.GetActiveRecordingInfo(path); + } + public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, User user = null) { var recording = (ILiveTvRecording)item; @@ -1949,27 +1950,72 @@ namespace Emby.Server.Implementations.LiveTv dto.IsKids = info.IsKids; dto.IsPremiere = info.IsPremiere; - dto.CanDelete = user == null - ? recording.CanDelete() - : recording.CanDelete(user); - - if (dto.MediaSources == null) + if (info.Status == RecordingStatus.InProgress && info.EndDate.HasValue) { - dto.MediaSources = recording.GetMediaSources(true); + var now = DateTime.UtcNow.Ticks; + var start = info.StartDate.Ticks; + var end = info.EndDate.Value.Ticks; + + var pct = now - start; + pct /= end; + pct *= 100; + dto.CompletionPercentage = pct; } - if (dto.MediaStreams == null) + if (channel != null) { - dto.MediaStreams = dto.MediaSources.SelectMany(i => i.MediaStreams).ToArray(); + dto.ChannelName = channel.Name; + + if (channel.HasImage(ImageType.Primary)) + { + dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); + } } + } - if (info.Status == RecordingStatus.InProgress && info.EndDate.HasValue) + 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 : GetInternalChannel(_tvDtoService.GetInternalChannelId(service.Name, info.ChannelId)); + + dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) + ? null + : _tvDtoService.GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"); + + dto.TimerId = string.IsNullOrEmpty(info.Id) + ? null + : _tvDtoService.GetInternalTimerId(service.Name, info.Id).ToString("N"); + + 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 = info.StartDate.Ticks; - var end = info.EndDate.Value.Ticks; + var start = startDate.Ticks; + var end = endDate.Ticks; var pct = now - start; + pct /= end; pct *= 100; dto.CompletionPercentage = pct; @@ -2098,7 +2144,6 @@ namespace Emby.Server.Implementations.LiveTv 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 return service.DeleteRecordingAsync(GetItemExternalId(recording), CancellationToken.None); } @@ -2348,7 +2393,6 @@ namespace Emby.Server.Implementations.LiveTv var currentChannelsDict = new Dictionary(); var addCurrentProgram = options.AddCurrentProgram; - var addMediaSources = options.Fields.Contains(ItemFields.MediaSources); var addServiceName = options.Fields.Contains(ItemFields.ServiceName); foreach (var tuple in tuples) @@ -2367,11 +2411,6 @@ namespace Emby.Server.Implementations.LiveTv currentChannelsDict[dto.Id] = dto; - if (addMediaSources) - { - dto.MediaSources = channel.GetMediaSources(true); - } - if (addCurrentProgram) { var channelIdString = channel.Id.ToString("N"); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 5436a12b8d..0e52f874d4 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -43,9 +43,11 @@ namespace Emby.Server.Implementations.LiveTv if (baseItem.SourceType == SourceType.LiveTV) { - if (string.IsNullOrWhiteSpace(baseItem.Path)) + var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path); + + if (string.IsNullOrWhiteSpace(baseItem.Path) || activeRecordingInfo != null) { - return GetMediaSourcesInternal(item, cancellationToken); + return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken); } } @@ -56,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv private const char StreamIdDelimeter = '_'; private const string StreamIdDelimeterString = "_"; - private async Task> GetMediaSourcesInternal(IHasMediaSources item, CancellationToken cancellationToken) + private async Task> GetMediaSourcesInternal(IHasMediaSources item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) { IEnumerable sources; @@ -67,12 +69,20 @@ namespace Emby.Server.Implementations.LiveTv if (item is ILiveTvRecording) { sources = await _liveTvManager.GetRecordingMediaSources(item, cancellationToken) - .ConfigureAwait(false); + .ConfigureAwait(false); } else { - sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken) - .ConfigureAwait(false); + if (activeRecordingInfo != null) + { + sources = await EmbyTV.EmbyTV.Current.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken) + .ConfigureAwait(false); + } + else + { + sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken) + .ConfigureAwait(false); + } } } catch (NotImplementedException) diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index bf4acdfbd6..54786134f3 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities { - public interface IHasMediaSources : IHasUserData + public interface IHasMediaSources : IHasMetadata { /// /// Gets the media sources. diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index fa11787f51..3918ac8fc1 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -160,7 +160,7 @@ namespace MediaBrowser.Controller.Entities public string[] GetPlayableStreamFileNames() { - return GetPlayableStreamFiles().Select(System.IO.Path.GetFileName).ToArray(); + return GetPlayableStreamFiles().Select(System.IO.Path.GetFileName).ToArray(); } /// @@ -234,6 +234,35 @@ namespace MediaBrowser.Controller.Entities return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); } + [IgnoreDataMember] + public override SourceType SourceType + { + get + { + if (IsActiveRecording()) + { + return SourceType.LiveTV; + } + + return base.SourceType; + } + } + + protected bool IsActiveRecording() + { + return LiveTvManager.GetActiveRecordingInfo(Path) != null; + } + + public override bool CanDelete() + { + if (IsActiveRecording()) + { + return false; + } + + return base.CanDelete(); + } + [IgnoreDataMember] protected virtual bool EnableDefaultVideoUserDataKeys { @@ -616,6 +645,14 @@ namespace MediaBrowser.Controller.Entities var list = GetAllVideosForMediaSources(); var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item1, i.Item2)).ToList(); + if (IsActiveRecording()) + { + foreach (var mediaSource in result) + { + mediaSource.Type = MediaSourceType.Placeholder; + } + } + return result.OrderBy(i => { if (i.VideoType == VideoType.VideoFile) diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 862894f613..6ff630590a 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -384,5 +384,9 @@ namespace MediaBrowser.Controller.LiveTv string GetEmbyTvActiveRecordingPath(string id); Task GetEmbyTvLiveStream(string id); + + ActiveRecordingInfo GetActiveRecordingInfo(string path); + + void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs index 43fc307f48..4b757f0b9b 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs @@ -36,4 +36,13 @@ namespace MediaBrowser.Controller.LiveTv DateTime? EndDate { get; set; } DateTime DateCreated { get; set; } } + + public class ActiveRecordingInfo + { + public string Id { get; set; } + public string Path { get; set; } + public TimerInfo Timer { get; set; } + public ProgramInfo Program { get; set; } + public CancellationTokenSource CancellationTokenSource { get; set; } + } } diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index 0b94c85fae..a0002241db 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -109,6 +109,9 @@ namespace MediaBrowser.Controller.LiveTv public bool IsKids { get; set; } public bool IsSports { get; set; } public bool IsNews { get; set; } + public bool IsSeries { get; set; } + public bool IsLive { get; set; } + public bool IsPremiere { get; set; } public int? ProductionYear { get; set; } public string EpisodeTitle { get; set; } public DateTime? OriginalAirDate { get; set; }