From d375758b21cfcba71d6d5b72a44be45d475cc3b2 Mon Sep 17 00:00:00 2001 From: emveepee Date: Tue, 21 May 2024 12:10:18 -0400 Subject: [PATCH] Initial changes for 10.9.1 (#48) --- .gitignore | 2 -- Directory.Build.props | 6 ++--- .../Helpers/ChannelHelper.cs | 17 ++++++++++++ .../Helpers/GenreMapper.cs | 22 ++++++++-------- .../{GeneralHelpers.cs => UtilsHelper.cs} | 18 +------------ .../Jellyfin.Plugin.NextPVR.csproj | 11 ++++---- Jellyfin.Plugin.NextPVR/LiveTvService.cs | 14 ++++++---- Jellyfin.Plugin.NextPVR/RecordingsChannel.cs | 22 +++++++--------- ...se.cs => CancelDeleteRecordingResponse.cs} | 4 +-- .../Responses/ChannelResponse.cs | 6 ++--- .../Responses/InitializeResponse.cs | 4 +-- .../Responses/LastUpdateResponse.cs | 26 +++++++++---------- .../Responses/ListingsResponse.cs | 4 +-- .../Responses/RecordingResponse.cs | 6 ++--- .../Responses/RecurringResponse.cs | 8 +++--- .../Responses/SettingResponse.cs | 6 ++--- .../Responses/TunerResponse.cs | 23 +++++++++------- Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs | 23 ++++++++++++++++ README.md | 2 +- build.yaml | 6 ++--- 20 files changed, 127 insertions(+), 103 deletions(-) create mode 100644 Jellyfin.Plugin.NextPVR/Helpers/ChannelHelper.cs rename Jellyfin.Plugin.NextPVR/Helpers/{GeneralHelpers.cs => UtilsHelper.cs} (58%) rename Jellyfin.Plugin.NextPVR/Responses/{CancelRecordingResponse.cs => CancelDeleteRecordingResponse.cs} (93%) create mode 100644 Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs diff --git a/.gitignore b/.gitignore index dd8e384..20a039d 100644 --- a/.gitignore +++ b/.gitignore @@ -230,5 +230,3 @@ pip-log.txt .mr.developer.cfg .idea -artifacts -.idea diff --git a/Directory.Build.props b/Directory.Build.props index c702921..9257d94 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 0.0.0.0 - 0.0.0.0 - 0.0.0.0 + 9.0.0.0 + 9.0.0.0 + 9.0.0.0 diff --git a/Jellyfin.Plugin.NextPVR/Helpers/ChannelHelper.cs b/Jellyfin.Plugin.NextPVR/Helpers/ChannelHelper.cs new file mode 100644 index 0000000..2933ecf --- /dev/null +++ b/Jellyfin.Plugin.NextPVR/Helpers/ChannelHelper.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.LiveTv; + +namespace Jellyfin.Plugin.NextPVR.Helpers; +public static class ChannelHelper +{ + public static ChannelType GetChannelType(int channelType) + { + ChannelType type = channelType switch + { + 1 => ChannelType.TV, + 10 => ChannelType.Radio, + _ => ChannelType.TV + }; + + return type; + } +} diff --git a/Jellyfin.Plugin.NextPVR/Helpers/GenreMapper.cs b/Jellyfin.Plugin.NextPVR/Helpers/GenreMapper.cs index 65b35fc..20a0be9 100644 --- a/Jellyfin.Plugin.NextPVR/Helpers/GenreMapper.cs +++ b/Jellyfin.Plugin.NextPVR/Helpers/GenreMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Plugin.NextPVR.Configuration; @@ -45,29 +45,29 @@ public class GenreMapper { if (genreMappings != null) { - if (_configuration.GenreMappings.ContainsKey(GenreMovie) && _configuration.GenreMappings[GenreMovie] != null) + if (_configuration.GenreMappings.TryGetValue(GenreMovie, out var value) && value != null) { - _movieGenres.AddRange(_configuration.GenreMappings[GenreMovie]); + _movieGenres.AddRange(value); } - if (_configuration.GenreMappings.ContainsKey(GenreSport) && _configuration.GenreMappings[GenreSport] != null) + if (_configuration.GenreMappings.TryGetValue(GenreSport, out value) && value != null) { - _sportGenres.AddRange(_configuration.GenreMappings[GenreSport]); + _sportGenres.AddRange(value); } - if (_configuration.GenreMappings.ContainsKey(GenreNews) && _configuration.GenreMappings[GenreNews] != null) + if (_configuration.GenreMappings.TryGetValue(GenreNews, out value) && value != null) { - _newsGenres.AddRange(_configuration.GenreMappings[GenreNews]); + _newsGenres.AddRange(value); } - if (_configuration.GenreMappings.ContainsKey(GenreKids) && _configuration.GenreMappings[GenreKids] != null) + if (_configuration.GenreMappings.TryGetValue(GenreKids, out value) && value != null) { - _kidsGenres.AddRange(_configuration.GenreMappings[GenreKids]); + _kidsGenres.AddRange(value); } - if (_configuration.GenreMappings.ContainsKey(GenreLive) && _configuration.GenreMappings[GenreLive] != null) + if (_configuration.GenreMappings.TryGetValue(GenreLive, out value) && value != null) { - _liveGenres.AddRange(_configuration.GenreMappings[GenreLive]); + _liveGenres.AddRange(value); } } } diff --git a/Jellyfin.Plugin.NextPVR/Helpers/GeneralHelpers.cs b/Jellyfin.Plugin.NextPVR/Helpers/UtilsHelper.cs similarity index 58% rename from Jellyfin.Plugin.NextPVR/Helpers/GeneralHelpers.cs rename to Jellyfin.Plugin.NextPVR/Helpers/UtilsHelper.cs index fc6ccd5..dac7d18 100644 --- a/Jellyfin.Plugin.NextPVR/Helpers/GeneralHelpers.cs +++ b/Jellyfin.Plugin.NextPVR/Helpers/UtilsHelper.cs @@ -1,23 +1,7 @@ -using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.NextPVR.Helpers; - -public static class ChannelHelper -{ - public static ChannelType GetChannelType(int channelType) - { - ChannelType type = channelType switch - { - 1 => ChannelType.TV, - 10 => ChannelType.Radio, - _ => ChannelType.TV - }; - - return type; - } -} - public static class UtilsHelper { public static void DebugInformation(ILogger logger, string message) diff --git a/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj b/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj index 24682bb..0b5d2dd 100644 --- a/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj +++ b/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj @@ -1,7 +1,9 @@ - net6.0 + net8.0 + 6.0.0.0 + 6.0.0.0 true AllEnabledByDefault true @@ -20,12 +22,9 @@ - - - - + - + diff --git a/Jellyfin.Plugin.NextPVR/LiveTvService.cs b/Jellyfin.Plugin.NextPVR/LiveTvService.cs index 8e45029..62895fa 100644 --- a/Jellyfin.Plugin.NextPVR/LiveTvService.cs +++ b/Jellyfin.Plugin.NextPVR/LiveTvService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Net.Http; using System.Security.Cryptography; using System.Text; @@ -35,10 +36,13 @@ public class LiveTvService : ILiveTvService _httpClientFactory = httpClientFactory; _logger = logger; LastUpdatedSidDateTime = DateTime.UtcNow; + Instance = this; } private string Sid { get; set; } + public static LiveTvService Instance { get; private set; } + public bool IsActive => Sid != null; private DateTimeOffset LastUpdatedSidDateTime { get; set; } @@ -58,7 +62,7 @@ public class LiveTvService : ILiveTvService /// /// The cancellation token. /// A representing the asynchronous operation. - private async Task EnsureConnectionAsync(CancellationToken cancellationToken) + public async Task EnsureConnectionAsync(CancellationToken cancellationToken) { var config = Plugin.Instance.Configuration; @@ -132,7 +136,7 @@ public class LiveTvService : ILiveTvService private string GetMd5Hash(string value) { #pragma warning disable CA5351 - var hashValue = MD5.Create().ComputeHash(new UTF8Encoding().GetBytes(value)); + var hashValue = MD5.HashData(new UTF8Encoding().GetBytes(value)); #pragma warning restore CA5351 // Bit convertor return the byte to string as all caps hex values separated by "-" return BitConverter.ToString(hashValue).Replace("-", string.Empty, StringComparison.Ordinal).ToLowerInvariant(); @@ -587,19 +591,19 @@ public class LiveTvService : ILiveTvService throw new NotImplementedException(); } - public Task GetChannelImageAsync(string channelId, CancellationToken cancellationToken) + public Task GetChannelImageAsync(string channelId, CancellationToken cancellationToken) { // Leave as is. This is handled by supplying image url to ChannelInfo throw new NotImplementedException(); } - public Task GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken) + public Task GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken) { // Leave as is. This is handled by supplying image url to ProgramInfo throw new NotImplementedException(); } - public Task GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken) + public Task GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken) { // Leave as is. This is handled by supplying image url to RecordingInfo throw new NotImplementedException(); diff --git a/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs b/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs index dcd40d3..7e38a24 100644 --- a/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs +++ b/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -8,7 +8,6 @@ using Jellyfin.Plugin.NextPVR.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; @@ -20,7 +19,6 @@ namespace Jellyfin.Plugin.NextPVR; public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISupportsLatestMedia, ISupportsMediaProbe, IHasFolderAttributes, IDisposable { - private readonly ILiveTvManager _liveTvManager; private readonly CancellationTokenSource _cancellationToken; private Timer _updateTimer; private DateTimeOffset _lastUpdate = DateTimeOffset.FromUnixTimeSeconds(0); @@ -28,9 +26,8 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo private IEnumerable _allRecordings; private bool _useCachedRecordings; - public RecordingsChannel(ILiveTvManager liveTvManager) + public RecordingsChannel() { - _liveTvManager = liveTvManager; var interval = TimeSpan.FromSeconds(20); _updateTimer = new Timer(OnUpdateTimerCallbackAsync, null, interval, interval); if (_updateTimer != null) @@ -116,7 +113,13 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo private LiveTvService GetService() { - return _liveTvManager.Services.OfType().FirstOrDefault(); + LiveTvService service = LiveTvService.Instance; + if (service is not null && !service.IsActive) + { + service.EnsureConnectionAsync(new System.Threading.CancellationToken(false)).Wait(); + } + + return service; } public bool CanDelete(BaseItem item) @@ -338,12 +341,7 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo private async void OnUpdateTimerCallbackAsync(object state) { var service = GetService(); - if (service is null) - { - return; - } - - if (service.IsActive) + if (service is not null && service.IsActive) { var backendUpdate = await service.GetLastUpdate(_cancellationToken.Token).ConfigureAwait(false); if (backendUpdate > _lastUpdate) diff --git a/Jellyfin.Plugin.NextPVR/Responses/CancelRecordingResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/CancelDeleteRecordingResponse.cs similarity index 93% rename from Jellyfin.Plugin.NextPVR/Responses/CancelRecordingResponse.cs rename to Jellyfin.Plugin.NextPVR/Responses/CancelDeleteRecordingResponse.cs index bf84fda..15a5e8c 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/CancelRecordingResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/CancelDeleteRecordingResponse.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; @@ -24,7 +24,7 @@ public class CancelDeleteRecordingResponse return false; } - private class RootObject + private sealed class RootObject { public string Stat { get; set; } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/ChannelResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/ChannelResponse.cs index 47f1018..85feda7 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/ChannelResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/ChannelResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -49,7 +49,7 @@ public class ChannelResponse } // Classes created with http://json2csharp.com/ - private class Channel + private sealed class Channel { public int ChannelId { get; set; } @@ -68,7 +68,7 @@ public class ChannelResponse public bool ChannelIcon { get; set; } } - private class RootObject + private sealed class RootObject { public List Channels { get; set; } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/InitializeResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/InitializeResponse.cs index 647a539..59fbc7e 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/InitializeResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/InitializeResponse.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; @@ -25,7 +25,7 @@ public class InitializeResponse throw new JsonException("Failed to validate your connection with NextPVR."); } - private class RootObject + private sealed class RootObject { public string Stat { get; set; } diff --git a/Jellyfin.Plugin.NextPVR/Responses/LastUpdateResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/LastUpdateResponse.cs index 145bb3e..967ae03 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/LastUpdateResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/LastUpdateResponse.cs @@ -19,18 +19,16 @@ public class LastUpdateResponse UtilsHelper.DebugInformation(logger, $"[NextPVR] LastUpdate Response: {JsonSerializer.Serialize(root, _jsonOptions)}"); return DateTimeOffset.FromUnixTimeSeconds(root.LastUpdate); } -} - -// Classes created with http://json2csharp.com/ - -public class RootObject -{ - [JsonPropertyName("last_update")] - public int LastUpdate { get; set; } - - public string Stat { get; set; } - - public int Code { get; set; } - - public string Msg { get; set; } + + private sealed class RootObject + { + [JsonPropertyName("last_update")] + public int LastUpdate { get; set; } + + public string Stat { get; set; } + + public int Code { get; set; } + + public string Msg { get; set; } + } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/ListingsResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/ListingsResponse.cs index 27f6d66..8877c33 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/ListingsResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/ListingsResponse.cs @@ -79,7 +79,7 @@ public class ListingsResponse // Classes created with http://json2csharp.com/ - private class Listing + private sealed class Listing { public int Id { get; set; } @@ -114,7 +114,7 @@ public class ListingsResponse public int RecordingId { get; set; } } - private class RootObject + private sealed class RootObject { public List Listings { get; set; } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs index 4d32ece..5a46bd5 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs @@ -98,7 +98,7 @@ public class RecordingResponse } else { - info.Url = $"{_baseUrl}/live?recording={i.Id}"; + info.Url = $"{_baseUrl}/live?recording={i.Id}&sid=jellyfin"; } } @@ -215,7 +215,7 @@ public class RecordingResponse return RecordingStatus.New; } - private class Recording + private sealed class Recording { public int Id { get; set; } @@ -280,7 +280,7 @@ public class RecordingResponse public int? Year { get; set; } } - private class RootObject + private sealed class RootObject { public List Recordings { get; set; } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/RecurringResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/RecurringResponse.cs index 3bf3e1b..08cf3dd 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/RecurringResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/RecurringResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.NextPVR.Responses; -internal class RecurringResponse +internal sealed class RecurringResponse { private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.CamelCaseOptions; @@ -70,7 +70,7 @@ internal class RecurringResponse return info; } - private class Recurring + private sealed class Recurring { public int Id { get; set; } @@ -107,7 +107,7 @@ internal class RecurringResponse public string AdvancedRules { get; set; } } - private class RootObject + private sealed class RootObject { public List Recurrings { get; set; } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs index 3491888..b26bc0e 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.IO; using System.Text.Json; using System.Threading.Tasks; @@ -31,7 +31,7 @@ public class SettingResponse // Classes created with http://json2csharp.com/ - private class ScheduleSettings + private sealed class ScheduleSettings { public string Version { get; set; } @@ -78,7 +78,7 @@ public class SettingResponse public int TimeEpoch { get; set; } } - private class SettingValue + private sealed class SettingValue { public string Value { get; set; } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/TunerResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/TunerResponse.cs index 58ccb4f..b737668 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/TunerResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/TunerResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -14,27 +14,29 @@ public class TunerResponse { private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.CamelCaseOptions; - public async Task> LiveTvTunerInfos(Stream stream) + public async Task> LiveTvTunerInfo(Stream stream) { var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false); return root.Tuners.Select(GetTunerInformation).ToList(); } - private LiveTvTunerInfo GetTunerInformation(Tuner i) + private TunerHostInfo GetTunerInformation(Tuner i) { - LiveTvTunerInfo tunerinfo = new LiveTvTunerInfo(); + TunerHostInfo tunerinfo = new TunerHostInfo(); - tunerinfo.Name = i.TunerName; + tunerinfo.FriendlyName = i.TunerName; + /* tunerinfo.Status = GetStatus(i); if (i.Recordings.Count > 0) { tunerinfo.ChannelId = i.Recordings.Single().Recording.ChannelOid.ToString(CultureInfo.InvariantCulture); } - + */ return tunerinfo; } + /* private LiveTvTunerStatus GetStatus(Tuner i) { if (i.Recordings.Count > 0) @@ -49,8 +51,9 @@ public class TunerResponse return LiveTvTunerStatus.Available; } + */ - private class Recording + private sealed class Recording { public int TunerOid { get; set; } @@ -61,12 +64,12 @@ public class TunerResponse public int RecordingOid { get; set; } } - private class Recordings + private sealed class Recordings { public Recording Recording { get; set; } } - private class Tuner + private sealed class Tuner { public string TunerName { get; set; } @@ -77,7 +80,7 @@ public class TunerResponse public List LiveTv { get; set; } } - private class RootObject + private sealed class RootObject { public List Tuners { get; set; } } diff --git a/Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs b/Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs new file mode 100644 index 0000000..88ce185 --- /dev/null +++ b/Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs @@ -0,0 +1,23 @@ +using Jellyfin.Plugin.NextPVR; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Plugin.NextPVR; + +/// +/// Register NextPVR services. +/// +/// +public class ServiceRegistrator : IPluginServiceRegistrator +{ + /// + public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + } +} diff --git a/README.md b/README.md index 2048578..4613b79 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This plugin provides access to live TV, program guide, and recordings from a [Ne ## Build -1. To build this plugin you will need [.Net 5.x](https://dotnet.microsoft.com/download/dotnet/5.0). +1. To build this plugin you will need [.Net 8.x](https://dotnet.microsoft.com/download/dotnet/8.0). 2. Build plugin with following command ``` diff --git a/build.yaml b/build.yaml index d52199a..4895d82 100644 --- a/build.yaml +++ b/build.yaml @@ -1,9 +1,9 @@ name: "NextPVR" guid: "9574ac10-bf23-49bc-949f-924f23cfa48f" imageUrl: "https://repo.jellyfin.org/releases/plugin/images/jellyfin-plugin-nextpvr.png" -version: "8" -targetAbi: "10.8.0.0" -framework: "net6.0" +version: 9 +targetAbi: "10.9.0.0" +framework: "net8.0" overview: "Live TV plugin for NextPVR" description: > Provides access to live TV, program guide, and recordings from NextPVR.