Recording enhancements

Enhanced recording security , configuration and polling, in-progress playback,  allow client direct play.
This commit is contained in:
Martin Vallevand 2024-06-08 13:20:10 -04:00
parent 089b919e54
commit cccea1622f
10 changed files with 458 additions and 222 deletions

View File

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>9.0.0.0</Version>
<AssemblyVersion>9.0.0.0</AssemblyVersion>
<FileVersion>9.0.0.0</FileVersion>
<Version>10.0.0.0</Version>
<AssemblyVersion>10.0.0.0</AssemblyVersion>
<FileVersion>10.0.0.0</FileVersion>
</PropertyGroup>
</Project>

View File

@ -19,6 +19,9 @@ public class PluginConfiguration : BasePluginConfiguration
NewEpisodes = false;
RecordingDefault = "2";
RecordingTransport = 1;
EnableInProgress = false;
PollInterval = 20;
BackendVersion = 0;
// Initialise this
GenreMappings = new SerializableDictionary<string, List<string>>();
GenreMappings["GENRESPORT"] = new List<string>()
@ -38,10 +41,20 @@ public class PluginConfiguration : BasePluginConfiguration
public string WebServiceUrl { get; set; }
public string CurrentWebServiceURL { get; set; }
public int BackendVersion { get; set; }
public string Pin { get; set; }
public string StoredSid { get; set; }
public bool EnableDebugLogging { get; set; }
public bool EnableInProgress { get; set; }
public int PollInterval { get; set; }
public bool NewEpisodes { get; set; }
public bool ShowRepeat { get; set; }
@ -56,9 +69,7 @@ public class PluginConfiguration : BasePluginConfiguration
public int PostPaddingSeconds { get; set; }
public string StoredSid { get; set; }
public DateTime SidModified { get; set; }
public DateTime RecordingModificationTime { get; set; }
/// <summary>
/// Gets or sets the genre mappings, to map localised NextPVR genres, to Jellyfin categories.

View File

@ -7,6 +7,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<NoWarn>CA2227;CA1002;CA2007;CS1591</NoWarn>
<Version>10.0.0.0</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
@ -17,6 +19,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
namespace Jellyfin.Plugin.NextPVR;
@ -26,12 +29,16 @@ namespace Jellyfin.Plugin.NextPVR;
public class LiveTvService : ILiveTvService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly bool _enableIPv6;
private readonly ILogger<LiveTvService> _logger;
private int _liveStreams;
private DateTimeOffset _lastRecordingChange = DateTimeOffset.MinValue;
public LiveTvService(IHttpClientFactory httpClientFactory, ILogger<LiveTvService> logger)
private string baseUrl;
public LiveTvService(IHttpClientFactory httpClientFactory, ILogger<LiveTvService> logger, IConfigurationManager configuration)
{
_enableIPv6 = configuration.GetNetworkConfiguration().EnableIPv6;
_httpClientFactory = httpClientFactory;
_logger = logger;
LastUpdatedSidDateTime = DateTime.UtcNow;
@ -40,12 +47,14 @@ public class LiveTvService : ILiveTvService
public string Sid { get; set; }
public DateTime DateRecordingModified { get; set; }
public DateTime RecordingModificationTime { get; set; }
public static LiveTvService Instance { get; private set; }
public bool IsActive => Sid != null;
public bool FlagRecordingChange { get; set; }
private DateTimeOffset LastUpdatedSidDateTime { get; set; }
/// <summary>
@ -63,27 +72,43 @@ public class LiveTvService : ILiveTvService
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task EnsureConnectionAsync(CancellationToken cancellationToken)
public async Task<bool> EnsureConnectionAsync(CancellationToken cancellationToken)
{
var config = Plugin.Instance.Configuration;
{
if (string.IsNullOrEmpty(config.WebServiceUrl))
if (!Uri.IsWellFormedUriString(config.WebServiceUrl, UriKind.Absolute))
{
_logger.LogError("[NextPVR] Web service url must be configured");
throw new InvalidOperationException("NextPvr web service url must be configured.");
_logger.LogError("[NextPVR] Web service URL must be configured");
throw new InvalidOperationException("NextPVR web service URL must be configured.");
}
if (string.IsNullOrEmpty(config.Pin))
{
_logger.LogError("[NextPVR] Pin must be configured");
throw new InvalidOperationException("NextPvr pin must be configured.");
_logger.LogError("[NextPVR] PIN must be configured");
throw new InvalidOperationException("NextPVR PIN must be configured.");
}
if (string.IsNullOrEmpty(Sid) || ((!string.IsNullOrEmpty(Sid)) && (LastUpdatedSidDateTime.AddMinutes(5) < DateTime.UtcNow)))
if (string.IsNullOrEmpty(config.StoredSid))
{
await InitiateSession(cancellationToken).ConfigureAwait(false);
Sid = null;
LastUpdatedSidDateTime = DateTimeOffset.MinValue;
}
if (string.IsNullOrEmpty(Sid) || ((!string.IsNullOrEmpty(Sid)) && (LastUpdatedSidDateTime.AddMinutes(5) < DateTimeOffset.UtcNow)) || RecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime)
{
try
{
await InitiateSession(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
Sid = null;
_logger.LogError("{0}", ex.Message);
}
}
}
return IsActive;
}
/// <summary>
@ -92,47 +117,79 @@ public class LiveTvService : ILiveTvService
private async Task InitiateSession(CancellationToken cancellationToken)
{
_logger.LogInformation("[NextPVR] Start InitiateSession");
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
baseUrl = Plugin.Instance.Configuration.CurrentWebServiceURL;
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.StoredSid) && Plugin.Instance.Configuration.SidModified != DateTime.MinValue)
httpClient.Timeout = TimeSpan.FromSeconds(5);
bool updateConfiguration = false;
bool validConfiguration = false;
if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.StoredSid) && !string.IsNullOrEmpty(Plugin.Instance.Configuration.CurrentWebServiceURL) )
{
string request = $"{baseUrl}/service?method=session.valid&device=jellyfin&sid={Plugin.Instance.Configuration.StoredSid}";
await using var vstream = await httpClient.GetStreamAsync(request, cancellationToken).ConfigureAwait(false);
bool valid = await new InitializeResponse().LoggedIn(vstream, _logger).ConfigureAwait(false);
if (valid)
{
Sid = Plugin.Instance.Configuration.StoredSid;
DateRecordingModified = Plugin.Instance.Configuration.SidModified;
LastUpdatedSidDateTime = DateTimeOffset.UtcNow;
_logger.LogInformation("[NextPVR] Valid sid: {0}", Sid);
return;
}
await using var stream = await httpClient.GetStreamAsync(request, cancellationToken).ConfigureAwait(false);
validConfiguration = await new InitializeResponse().LoggedIn(stream, _logger).ConfigureAwait(false);
}
await using var stream = await httpClient.GetStreamAsync($"{baseUrl}/service?method=session.initiate&ver=1.0&device=jellyfin", cancellationToken).ConfigureAwait(false);
var clientKeys = await new InstantiateResponse().GetClientKeys(stream, _logger).ConfigureAwait(false);
var sid = clientKeys.Sid;
var salt = clientKeys.Salt;
_logger.LogInformation("[NextPVR] Sid: {0}", sid);
var loggedIn = await Login(sid, salt, cancellationToken).ConfigureAwait(false);
if (loggedIn)
if (!validConfiguration)
{
UriBuilder builder = new UriBuilder(Plugin.Instance.Configuration.WebServiceUrl);
if (!_enableIPv6 && builder.Host != "localhost" && builder.Host != "127.0.0.1")
{
if (builder.Host == "[::1]")
{
builder.Host = "127.0.0.1";
}
try
{
Uri uri = new Uri(Plugin.Instance.Configuration.WebServiceUrl);
var hosts = await Dns.GetHostEntryAsync(uri.Host, System.Net.Sockets.AddressFamily.InterNetwork, cancellationToken);
if (hosts != null)
{
var host = hosts.AddressList.FirstOrDefault().ToString();
if (builder.Host != host)
{
_logger.LogInformation("[NextPVR] Changed host from {0} to {1}", builder.Host, host);
builder.Host = host;
}
}
}
catch (Exception ex)
{
_logger.LogError("Could not resolve {0} {1}", Plugin.Instance.Configuration.WebServiceUrl, ex.Message);
}
}
baseUrl = builder.ToString().TrimEnd('/');
await using var stream = await httpClient.GetStreamAsync($"{baseUrl}/service?method=session.initiate&ver=1.0&device=jellyfin", cancellationToken).ConfigureAwait(false);
var clientKeys = await new InstantiateResponse().GetClientKeys(stream, _logger).ConfigureAwait(false);
var sid = clientKeys.Sid;
var salt = clientKeys.Salt;
validConfiguration = await Login(sid, salt, cancellationToken).ConfigureAwait(false);
Plugin.Instance.Configuration.StoredSid = sid;
updateConfiguration = true;
}
if (validConfiguration)
{
_logger.LogInformation("[NextPVR] Session initiated");
Sid = sid;
LastUpdatedSidDateTime = DateTimeOffset.UtcNow;
Plugin.Instance.Configuration.StoredSid = Sid;
Plugin.Instance.Configuration.SidModified = DateTime.Now;
Plugin.Instance.SaveConfiguration();
DateRecordingModified = Plugin.Instance.Configuration.SidModified;
Sid = Plugin.Instance.Configuration.StoredSid;
_logger.LogInformation("[NextPVR] Session initiated");
_logger.LogInformation("[NextPVR] Sid: {0}", Sid);
if (updateConfiguration)
{
Plugin.Instance.Configuration.CurrentWebServiceURL = baseUrl;
Plugin.Instance.Configuration.RecordingModificationTime = DateTime.UtcNow;
Plugin.Instance.SaveConfiguration();
}
RecordingModificationTime = Plugin.Instance.Configuration.RecordingModificationTime;
await GetDefaultSettingsAsync(cancellationToken).ConfigureAwait(false);
Plugin.Instance.Configuration.GetEpisodeImage = await GetBackendSettingAsync("/Settings/General/ArtworkFromSchedulesDirect", cancellationToken).ConfigureAwait(false) == "true";
}
else
{
Sid = null;
_logger.LogError("[NextPVR] PIN not accepted");
throw new UnauthorizedAccessException("NextPVR PIN not accepted");
}
@ -141,7 +198,6 @@ public class LiveTvService : ILiveTvService
private async Task<bool> Login(string sid, string salt, CancellationToken cancellationToken)
{
_logger.LogInformation("[NextPVR] Start Login procedure for Sid: {0} & Salt: {1}", sid, salt);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
var pin = Plugin.Instance.Configuration.Pin;
_logger.LogInformation("[NextPVR] PIN: {0}", pin == "0000" ? pin : "Not default");
@ -174,7 +230,6 @@ public class LiveTvService : ILiveTvService
_logger.LogInformation("[NextPVR] Start GetChannels Async, retrieve all channels");
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=channel.list&sid={Sid}", cancellationToken);
@ -190,9 +245,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start GetRecordings Async, retrieve all 'Pending', 'Inprogress' and 'Completed' recordings ");
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.list&filter=ready&sid={Sid}", cancellationToken);
return await new RecordingResponse(baseUrl, _logger).GetRecordings(stream).ConfigureAwait(false);
@ -208,9 +260,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start Delete Recording Async for recordingId: {RecordingId}", recordingId);
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.delete&recording_id={recordingId}&sid={Sid}", cancellationToken);
_lastRecordingChange = DateTimeOffset.UtcNow;
@ -222,6 +271,10 @@ public class LiveTvService : ILiveTvService
_logger.LogError("[NextPVR] Failed to delete the recording for recordingId: {RecordingId}", recordingId);
throw new JsonException($"Failed to delete the recording for recordingId: {recordingId}");
}
else
{
FlagRecordingChange = true;
}
_logger.LogInformation("[NextPVR] Deleted Recording with recordingId: {RecordingId}", recordingId);
}
@ -236,8 +289,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start Cancel Recording Async for recordingId: {TimerId}", timerId);
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.delete&recording_id={timerId}&sid={Sid}", cancellationToken);
@ -249,12 +300,16 @@ public class LiveTvService : ILiveTvService
_logger.LogError("[NextPVR] Failed to cancel the recording for recordingId: {TimerId}", timerId);
throw new JsonException($"Failed to cancel the recording for recordingId: {timerId}");
}
else
{
FlagRecordingChange = true;
}
_logger.LogInformation("[NextPVR] Cancelled Recording for recordingId: {TimerId}", timerId);
}
/// <summary>
/// Create a new recording.
/// Create a new scheduled recording.
/// </summary>
/// <param name="info">The TimerInfo.</param>
/// <param name="cancellationToken">The cancellationToken.</param>
@ -263,8 +318,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start CreateTimer Async for ChannelId: {ChannelId} & Name: {Name}", info.ChannelId, info.Name);
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
UtilsHelper.DebugInformation(_logger, $"[NextPVR] TimerSettings CreateTimer: {info.ProgramId} for ChannelId: {info.ChannelId} & Name: {info.Name}");
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync(
@ -284,24 +337,31 @@ public class LiveTvService : ILiveTvService
_logger.LogError("[NextPVR] Failed to create the timer with programId: {ProgramId}", info.ProgramId);
throw new JsonException($"Failed to create the timer with programId: {info.ProgramId}");
}
else if (info.StartDate <= DateTime.UtcNow)
{
FlagRecordingChange = true;
}
_logger.LogError("[NextPVR] CreateTimer async for programId: {ProgramId}", info.ProgramId);
}
/// <summary>
/// Get the pending Recordings.
/// Get the pending Timers.
/// </summary>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("[NextPVR] Start GetTimer Async, retrieve the 'Pending' recordings");
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.list&filter=pending&sid={Sid}", cancellationToken);
if (await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false))
{
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.list&filter=pending&sid={Sid}", cancellationToken);
return await new RecordingResponse(baseUrl, _logger).GetTimers(stream).ConfigureAwait(false);
return await new RecordingResponse(baseUrl, _logger).GetTimers(stream).ConfigureAwait(false);
}
return new List<TimerInfo>();
}
/// <summary>
@ -313,7 +373,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start GetSeriesTimer Async, retrieve the recurring recordings");
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.recurring.list&sid={Sid}", cancellationToken);
@ -330,7 +389,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start CreateSeriesTimer Async for ChannelId: {ChannelId} & Name: {Name}", info.ChannelId, info.Name);
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
var url = $"{baseUrl}/service?method=recording.recurring.save&sid={Sid}&pre_padding={info.PrePaddingSeconds / 60}&post_padding={info.PostPaddingSeconds / 60}&keep={info.KeepUpTo}";
int recurringType = int.Parse(Plugin.Instance.Configuration.RecordingDefault, CultureInfo.InvariantCulture);
@ -390,7 +448,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start UpdateSeriesTimer Async for ChannelId: {ChannelId} & Name: {Name}", info.ChannelId, info.Name);
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
var url = $"{baseUrl}/service?method=recording.recurring.save&sid={Sid}&pre_padding={info.PrePaddingSeconds / 60}&post_padding={info.PostPaddingSeconds / 60}&keep={info.KeepUpTo}&recurring_id={info.Id}";
@ -442,8 +499,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start UpdateTimer Async for ChannelId: {ChannelId} & Name: {Name}", updatedTimer.ChannelId, updatedTimer.Name);
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.save&sid={Sid}&pre_padding={updatedTimer.PrePaddingSeconds / 60}&post_padding={updatedTimer.PostPaddingSeconds / 60}&recording_id={updatedTimer.Id}&event_id={updatedTimer.ProgramId}", cancellationToken);
@ -467,8 +522,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start Cancel SeriesRecording Async for recordingId: {TimerId}", timerId);
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.recurring.delete&recurring_id={timerId}&sid={Sid}", cancellationToken);
@ -492,10 +545,16 @@ public class LiveTvService : ILiveTvService
public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
_logger.LogInformation("[NextPVR] Start ChannelStream");
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
_liveStreams++;
string streamUrl = $"{baseUrl}/live?channeloid={channelId}&client=jellyfin.{_liveStreams.ToString(CultureInfo.InvariantCulture)}&sid={Sid}";
string sidParameter = null;
if (Plugin.Instance.Configuration.RecordingTransport != 3)
{
sidParameter = $"&sid={Sid}";
}
string streamUrl = $"{baseUrl}/live?channeloid={channelId}&client=jellyfin.{_liveStreams.ToString(CultureInfo.InvariantCulture)}{sidParameter}";
_logger.LogInformation("[NextPVR] Streaming {Url}", streamUrl);
var mediaSourceInfo = new MediaSourceInfo
{
@ -508,7 +567,7 @@ public class LiveTvService : ILiveTvService
new MediaStream
{
Type = MediaStreamType.Video,
IsInterlaced = true,
// IsInterlaced = true,
// Set the index to -1 because we don't know the exact index of the video stream within the container
Index = -1,
},
@ -546,7 +605,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start GetDefaultSettings Async");
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=setting.list&sid={Sid}", cancellationToken);
return await new SettingResponse().GetDefaultSettings(stream, _logger).ConfigureAwait(false);
@ -556,7 +614,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] Start GetPrograms Async, retrieve all Programs");
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=channel.listings&sid={Sid}&start={((DateTimeOffset)startDateUtc).ToUnixTimeSeconds()}&end={((DateTimeOffset)endDateUtc).ToUnixTimeSeconds()}&channel_id={channelId}", cancellationToken);
return await new ListingsResponse(baseUrl).GetPrograms(stream, channelId, _logger).ConfigureAwait(false);
@ -571,29 +628,31 @@ public class LiveTvService : ILiveTvService
{
_logger.LogDebug("[NextPVR] GetLastUpdateTime");
DateTimeOffset retTime = DateTimeOffset.FromUnixTimeSeconds(0);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
try
{
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=recording.lastupdated&ignore_resume=true&sid={Sid}", cancellationToken);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
httpClient.Timeout = TimeSpan.FromSeconds(5);
var stream = await httpClient.GetStreamAsync($"{baseUrl}/service?method=recording.lastupdated&ignore_resume=true&sid={Sid}", cancellationToken);
retTime = await new LastUpdateResponse().GetUpdateTime(stream, _logger).ConfigureAwait(false);
if (retTime == DateTimeOffset.FromUnixTimeSeconds(0))
{
LastUpdatedSidDateTime = DateTimeOffset.MinValue;
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
}
else if (LastUpdatedSidDateTime != DateTimeOffset.MinValue)
{
LastUpdatedSidDateTime = DateTime.UtcNow;
LastUpdatedSidDateTime = DateTimeOffset.UtcNow;
}
UtilsHelper.DebugInformation(_logger, $"[NextPVR] GetLastUpdateTime {retTime.ToUnixTimeSeconds()}");
}
catch (HttpRequestException)
{
LastUpdatedSidDateTime = DateTimeOffset.MinValue;
_logger.LogWarning("Could not connect to servier");
Sid = null;
}
UtilsHelper.DebugInformation(_logger, $"[NextPVR] GetLastUpdateTime {retTime.ToUnixTimeSeconds()}");
return retTime;
}
@ -601,7 +660,6 @@ public class LiveTvService : ILiveTvService
{
_logger.LogInformation("[NextPVR] GetBackendSetting");
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
var baseUrl = Plugin.Instance.Configuration.WebServiceUrl;
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync($"{baseUrl}/service?method=setting.get&key={key}&sid={Sid}", cancellationToken);

View File

@ -1,39 +1,55 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.Plugin.NextPVR.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.NextPVR;
public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISupportsLatestMedia, ISupportsMediaProbe, IHasFolderAttributes, IDisposable
public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISupportsLatestMedia, ISupportsMediaProbe, IHasFolderAttributes, IDisposable, IHasItemChangeMonitor
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<RecordingsChannel> _logger;
private readonly CancellationTokenSource _cancellationToken;
private readonly string _recordingCacheDirectory;
private static SemaphoreSlim _semaphore;
private Timer _updateTimer;
private DateTimeOffset _lastUpdate = DateTimeOffset.FromUnixTimeSeconds(0);
private IEnumerable<MyRecordingInfo> _allRecordings;
private bool _useCachedRecordings;
private bool _useCachedRecordings = false;
private DateTime _cachedRecordingModificationTime;
private string _cachekeyBase;
private int _pollInterval = -1;
public RecordingsChannel()
public RecordingsChannel(IApplicationPaths applicationPaths, ILibraryManager libraryManager, IFileSystem fileSystem, ILogger<RecordingsChannel> logger)
{
var interval = TimeSpan.FromSeconds(20);
_updateTimer = new Timer(OnUpdateTimerCallbackAsync, null, interval, interval);
if (_updateTimer != null)
{
_cancellationToken = new CancellationTokenSource();
}
_fileSystem = fileSystem;
_logger = logger;
string channelId = libraryManager.GetNewItemId($"Channel {Name}", typeof(Channel)).ToString("N", CultureInfo.InvariantCulture);
string version = BaseExtensions.GetMD5($"{DataVersion}2").ToString("N", CultureInfo.InvariantCulture);
_recordingCacheDirectory = Path.Join(applicationPaths.CachePath, "channels", channelId, version);
CleanCache(true);
_cancellationToken = new CancellationTokenSource();
_semaphore = new SemaphoreSlim(1, 1);
}
public string Name => "NextPVR Recordings";
@ -69,21 +85,26 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo
public string GetCacheKey(string userId)
{
var now = DateTime.UtcNow;
DateTimeOffset dto = LiveTvService.Instance.RecordingModificationTime;
return $"{dto.ToUnixTimeSeconds()}-{_cachekeyBase}";
}
var values = new List<string>();
private void CleanCache(bool cleanAll = false)
{
if (!string.IsNullOrEmpty(_recordingCacheDirectory) && Directory.Exists(_recordingCacheDirectory))
{
string[] cachedJson = Directory.GetFiles(_recordingCacheDirectory, "*.json");
_logger.LogInformation("Cleaning JSON cache {0} {1}", _recordingCacheDirectory, cachedJson.Length);
foreach (string fileName in cachedJson)
{
if (cleanAll == true || _fileSystem.GetLastWriteTimeUtc(fileName).Add(TimeSpan.FromHours(3)) <= DateTimeOffset.UtcNow)
{
_fileSystem.DeleteFile(fileName);
}
}
}
values.Add(now.DayOfYear.ToString(CultureInfo.InvariantCulture));
values.Add(now.Hour.ToString(CultureInfo.InvariantCulture));
double minute = now.Minute;
minute /= 5;
values.Add(Math.Floor(minute).ToString(CultureInfo.InvariantCulture));
values.Add(GetService().LastRecordingChange.Ticks.ToString(CultureInfo.InvariantCulture));
return string.Join('-', values.ToArray());
return;
}
public InternalChannelFeatures GetChannelFeatures()
@ -114,9 +135,24 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo
private LiveTvService GetService()
{
LiveTvService service = LiveTvService.Instance;
if (service is not null && !service.IsActive)
if (service is not null && (!service.IsActive || _cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime || service.FlagRecordingChange))
{
service.EnsureConnectionAsync(new System.Threading.CancellationToken(false)).Wait();
try
{
CancellationToken cancellationToken = CancellationToken.None;
service.EnsureConnectionAsync(cancellationToken).Wait();
if (service.IsActive)
{
_useCachedRecordings = false;
if (_cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime)
{
_cachedRecordingModificationTime = Plugin.Instance.Configuration.RecordingModificationTime;
}
}
}
catch (Exception)
{
}
}
return service;
@ -124,11 +160,21 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo
public bool CanDelete(BaseItem item)
{
if (_cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime)
{
return false;
}
return !item.IsFolder;
}
public Task DeleteItem(string id, CancellationToken cancellationToken)
{
if (_cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime)
{
return Task.FromException(new InvalidOperationException("Recordings not reloaded"));
}
var service = GetService();
return service is null
? Task.CompletedTask
@ -142,61 +188,54 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo
return result.Items.OrderByDescending(i => i.DateCreated ?? DateTime.MinValue);
}
public Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
{
await GetRecordingsAsync("GetChannelItems", cancellationToken);
if (string.IsNullOrWhiteSpace(query.FolderId))
{
return GetRecordingGroups(query, cancellationToken);
return await GetRecordingGroups(query, cancellationToken);
}
if (query.FolderId.StartsWith("series_", StringComparison.OrdinalIgnoreCase))
{
var hash = query.FolderId.Split('_')[1];
return GetChannelItems(query, i => i.IsSeries && string.Equals(i.Name.GetMD5().ToString("N"), hash, StringComparison.Ordinal), cancellationToken);
return await GetChannelItems(query, i => i.IsSeries && string.Equals(i.Name.GetMD5().ToString("N"), hash, StringComparison.Ordinal), cancellationToken);
}
if (string.Equals(query.FolderId, "kids", StringComparison.OrdinalIgnoreCase))
{
return GetChannelItems(query, i => i.IsKids, cancellationToken);
return await GetChannelItems(query, i => i.IsKids, cancellationToken);
}
if (string.Equals(query.FolderId, "movies", StringComparison.OrdinalIgnoreCase))
{
return GetChannelItems(query, i => i.IsMovie, cancellationToken);
return await GetChannelItems(query, i => i.IsMovie, cancellationToken);
}
if (string.Equals(query.FolderId, "news", StringComparison.OrdinalIgnoreCase))
{
return GetChannelItems(query, i => i.IsNews, cancellationToken);
return await GetChannelItems(query, i => i.IsNews, cancellationToken);
}
if (string.Equals(query.FolderId, "sports", StringComparison.OrdinalIgnoreCase))
{
return GetChannelItems(query, i => i.IsSports, cancellationToken);
return await GetChannelItems(query, i => i.IsSports, cancellationToken);
}
if (string.Equals(query.FolderId, "others", StringComparison.OrdinalIgnoreCase))
{
return GetChannelItems(query, i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries, cancellationToken);
return await GetChannelItems(query, i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries, cancellationToken);
}
var result = new ChannelItemResult() { Items = new List<ChannelItemInfo>() };
return Task.FromResult(result);
return result;
}
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, Func<MyRecordingInfo, bool> filter, CancellationToken cancellationToken)
{
var service = GetService();
if (_useCachedRecordings == false)
{
_allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false);
await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false);
_useCachedRecordings = true;
}
await GetRecordingsAsync("GetChannelItems", cancellationToken);
List<ChannelItemInfo> pluginItems = new List<ChannelItemInfo>();
pluginItems.AddRange(_allRecordings.Where(filter).Select(ConvertToChannelItem));
var result = new ChannelItemResult() { Items = pluginItems };
@ -212,9 +251,11 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo
{
Name = string.IsNullOrEmpty(item.EpisodeTitle) ? item.Name : item.EpisodeTitle,
SeriesName = !string.IsNullOrEmpty(item.EpisodeTitle) || item.IsSeries ? item.Name : null,
StartDate = item.StartDate,
EndDate = item.EndDate,
OfficialRating = item.OfficialRating,
CommunityRating = item.CommunityRating,
ContentType = item.IsMovie ? ChannelMediaContentType.Movie : (item.IsSeries ? ChannelMediaContentType.Episode : ChannelMediaContentType.Clip),
ContentType = item.IsMovie ? ChannelMediaContentType.Movie : ChannelMediaContentType.Episode,
Genres = item.Genres,
ImageUrl = item.ImageUrl,
Id = item.Id,
@ -226,112 +267,164 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo
new MediaSourceInfo
{
Path = path,
Container = item.Status == RecordingStatus.InProgress ? "ts" : null,
Protocol = path.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? MediaProtocol.Http : MediaProtocol.File,
Id = item.Id,
BufferMs = 1000,
AnalyzeDurationMs = 0,
IsInfiniteStream = item.Status == RecordingStatus.InProgress,
RunTimeTicks = (item.EndDate - item.StartDate).Ticks,
TranscodingContainer = "ts",
RunTimeTicks = item.Status == RecordingStatus.InProgress ? null : (item.EndDate - item.StartDate).Ticks,
}
},
PremiereDate = item.OriginalAirDate,
ProductionYear = item.ProductionYear,
Type = ChannelItemType.Media,
DateModified = LiveTvService.Instance.DateRecordingModified,
DateModified = item.Status == RecordingStatus.InProgress ? DateTime.Now : Plugin.Instance.Configuration.RecordingModificationTime,
Overview = item.Overview,
IsLiveStream = item.Status == RecordingStatus.InProgress,
IsLiveStream = item.Status != RecordingStatus.InProgress ? false : Plugin.Instance.Configuration.EnableInProgress,
Etag = item.Status.ToString()
};
return channelItem;
}
private async Task<bool> GetRecordingsAsync(string name, CancellationToken cancellationToken)
{
var service = GetService();
if (service is null || !service.IsActive)
{
return false;
}
if (_useCachedRecordings == false || service.FlagRecordingChange)
{
if (_pollInterval == -1)
{
var interval = TimeSpan.FromSeconds(Plugin.Instance.Configuration.PollInterval);
_updateTimer = new Timer(OnUpdateTimerCallbackAsync, null, TimeSpan.FromMinutes(2), interval);
if (_updateTimer != null)
{
_pollInterval = Plugin.Instance.Configuration.PollInterval;
}
}
if (await _semaphore.WaitAsync(30000, cancellationToken))
{
try
{
_logger.LogDebug("{0} Reload cache", name);
_allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false);
int maxId = _allRecordings.Max(r => int.Parse(r.Id, CultureInfo.InvariantCulture));
int inProcessCount = _allRecordings.Where(r => r.Status == RecordingStatus.InProgress).Count();
string keyBase = $"{maxId}-{inProcessCount}-{_allRecordings.Count()}";
if (keyBase != _cachekeyBase && !service.FlagRecordingChange)
{
_logger.LogDebug("External recording list change {0}", keyBase);
CleanCache(true);
}
_cachekeyBase = keyBase;
_lastUpdate = DateTimeOffset.UtcNow;
service.FlagRecordingChange = false;
_useCachedRecordings = true;
}
catch (Exception)
{
}
_semaphore.Release();
}
}
return _useCachedRecordings;
}
private async Task<ChannelItemResult> GetRecordingGroups(InternalChannelItemQuery query, CancellationToken cancellationToken)
{
List<ChannelItemInfo> pluginItems = new List<ChannelItemInfo>();
var service = GetService();
if (_useCachedRecordings == false)
{
_allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false);
await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false);
_useCachedRecordings = true;
}
var series = _allRecordings
.Where(i => i.IsSeries)
.ToLookup(i => i.Name, StringComparer.OrdinalIgnoreCase);
pluginItems.AddRange(series.OrderBy(i => i.Key).Select(i => new ChannelItemInfo
if (await GetRecordingsAsync("GetRecordingGroups", cancellationToken))
{
Name = i.Key,
FolderType = ChannelFolderType.Container,
Id = "series_" + i.Key.GetMD5().ToString("N"),
Type = ChannelItemType.Folder,
DateCreated = i.Last().StartDate,
ImageUrl = i.Last().ImageUrl.Replace("=poster", "=landscape", StringComparison.OrdinalIgnoreCase)
}));
var kids = _allRecordings.FirstOrDefault(i => i.IsKids);
var series = _allRecordings
.Where(i => i.IsSeries)
.ToLookup(i => i.Name, StringComparer.OrdinalIgnoreCase);
if (kids != null)
{
pluginItems.Add(new ChannelItemInfo
pluginItems.AddRange(series.OrderBy(i => i.Key).Select(i => new ChannelItemInfo
{
Name = "Kids",
Name = i.Key,
FolderType = ChannelFolderType.Container,
Id = "kids",
Id = "series_" + i.Key.GetMD5().ToString("N"),
Type = ChannelItemType.Folder,
ImageUrl = kids.ImageUrl
});
}
DateCreated = i.Last().StartDate,
ImageUrl = i.Last().ImageUrl.Replace("=poster", "=landscape", StringComparison.OrdinalIgnoreCase)
}));
var movies = _allRecordings.FirstOrDefault(i => i.IsMovie);
if (movies != null)
{
pluginItems.Add(new ChannelItemInfo
{
Name = "Movies",
FolderType = ChannelFolderType.Container,
Id = "movies",
Type = ChannelItemType.Folder,
ImageUrl = movies.ImageUrl
});
}
var kids = _allRecordings.FirstOrDefault(i => i.IsKids);
var news = _allRecordings.FirstOrDefault(i => i.IsNews);
if (news != null)
{
pluginItems.Add(new ChannelItemInfo
if (kids != null)
{
Name = "News",
FolderType = ChannelFolderType.Container,
Id = "news",
Type = ChannelItemType.Folder,
ImageUrl = news.ImageUrl
});
}
pluginItems.Add(new ChannelItemInfo
{
Name = "Kids",
FolderType = ChannelFolderType.Container,
Id = "kids",
Type = ChannelItemType.Folder,
ImageUrl = kids.ImageUrl
});
}
var sports = _allRecordings.FirstOrDefault(i => i.IsSports);
if (sports != null)
{
pluginItems.Add(new ChannelItemInfo
var movies = _allRecordings.FirstOrDefault(i => i.IsMovie);
if (movies != null)
{
Name = "Sports",
FolderType = ChannelFolderType.Container,
Id = "sports",
Type = ChannelItemType.Folder,
ImageUrl = sports.ImageUrl
});
}
pluginItems.Add(new ChannelItemInfo
{
Name = "Movies",
FolderType = ChannelFolderType.Container,
Id = "movies",
Type = ChannelItemType.Folder,
ImageUrl = movies.ImageUrl
});
}
var other = _allRecordings.FirstOrDefault(i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries);
if (other != null)
{
pluginItems.Add(new ChannelItemInfo
var news = _allRecordings.FirstOrDefault(i => i.IsNews);
if (news != null)
{
Name = "Others",
FolderType = ChannelFolderType.Container,
Id = "others",
Type = ChannelItemType.Folder,
ImageUrl = other.ImageUrl
});
pluginItems.Add(new ChannelItemInfo
{
Name = "News",
FolderType = ChannelFolderType.Container,
Id = "news",
Type = ChannelItemType.Folder,
ImageUrl = news.ImageUrl
});
}
var sports = _allRecordings.FirstOrDefault(i => i.IsSports);
if (sports != null)
{
pluginItems.Add(new ChannelItemInfo
{
Name = "Sports",
FolderType = ChannelFolderType.Container,
Id = "sports",
Type = ChannelItemType.Folder,
ImageUrl = sports.ImageUrl
});
}
var other = _allRecordings.OrderByDescending(j => j.StartDate).FirstOrDefault(i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries);
if (other != null)
{
pluginItems.Add(new ChannelItemInfo
{
Name = "Others",
FolderType = ChannelFolderType.Container,
Id = "others",
Type = ChannelItemType.Folder,
DateModified = other.StartDate,
ImageUrl = other.ImageUrl
});
}
}
var result = new ChannelItemResult() { Items = pluginItems };
@ -340,15 +433,21 @@ public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISuppo
private async void OnUpdateTimerCallbackAsync(object state)
{
var service = GetService();
LiveTvService service = LiveTvService.Instance;
if (service is not null && service.IsActive)
{
var backendUpdate = await service.GetLastUpdate(_cancellationToken.Token).ConfigureAwait(false);
if (backendUpdate > _lastUpdate)
{
_logger.LogDebug("Recordings reset {0}", backendUpdate);
_useCachedRecordings = false;
_lastUpdate = backendUpdate;
await GetRecordingsAsync("OnUpdateTimerCallbackAsync", _cancellationToken.Token);
}
}
}
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
throw new NotImplementedException();
}
}

View File

@ -90,6 +90,7 @@ public class RecordingResponse
info.SeriesTimerId = i.RecurringParent.ToString(CultureInfo.InvariantCulture);
}
info.Status = ParseStatus(i.Status);
if (i.File != null)
{
if (Plugin.Instance.Configuration.RecordingTransport == 2)
@ -98,11 +99,23 @@ public class RecordingResponse
}
else
{
info.Url = $"{_baseUrl}/live?recording={i.Id}&sid={LiveTvService.Instance.Sid}";
string sidParameter = null;
if (Plugin.Instance.Configuration.RecordingTransport == 1 || Plugin.Instance.Configuration.BackendVersion < 60106)
{
sidParameter = $"&sid={LiveTvService.Instance.Sid}";
}
if (info.Status == RecordingStatus.InProgress)
{
info.Url = $"{_baseUrl}/live?recording={i.Id}{sidParameter}&growing=true";
}
else
{
info.Url = $"{_baseUrl}/live?recording={i.Id}{sidParameter}";
}
}
}
info.Status = ParseStatus(i.Status);
info.StartDate = DateTimeOffset.FromUnixTimeSeconds(i.StartTime).DateTime;
info.EndDate = DateTimeOffset.FromUnixTimeSeconds(i.StartTime + i.Duration).DateTime;
@ -121,6 +134,11 @@ public class RecordingResponse
info.SeasonNumber = i.Season;
info.EpisodeNumber = i.Episode;
info.IsSeries = true;
string se = string.Format(CultureInfo.InvariantCulture, "S{0:D2}E{1:D2} - ", i.Season, i.Episode);
if (i.Subtitle.StartsWith(se, StringComparison.CurrentCulture))
{
info.EpisodeTitle = i.Subtitle.Substring(se.Length);
}
}
if (i.Original != null)
@ -165,8 +183,18 @@ public class RecordingResponse
info.Name = i.Name;
info.Overview = i.Desc;
info.EpisodeTitle = i.Subtitle;
info.SeasonNumber = i.Season;
info.EpisodeNumber = i.Episode;
if (i.Season.HasValue)
{
info.SeasonNumber = i.Season;
info.EpisodeNumber = i.Episode;
info.IsSeries = true;
string se = string.Format(CultureInfo.InvariantCulture, "S{0:D2}E{1:D2} - ", i.Season, i.Episode);
if (i.Subtitle.StartsWith(se, StringComparison.CurrentCulture))
{
info.EpisodeTitle = i.Subtitle.Substring(se.Length);
}
}
info.OfficialRating = i.Rating;
if (i.Original != null)
{

View File

@ -1,6 +1,6 @@
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using Jellyfin.Plugin.NextPVR.Helpers;
@ -16,9 +16,10 @@ public class SettingResponse
{
var root = await JsonSerializer.DeserializeAsync<ScheduleSettings>(stream, _jsonOptions).ConfigureAwait(false);
UtilsHelper.DebugInformation(logger, $"[NextPVR] GetDefaultTimerInfo Response: {JsonSerializer.Serialize(root, _jsonOptions)}");
Plugin.Instance.Configuration.PostPaddingSeconds = int.Parse(root.PostPadding, CultureInfo.InvariantCulture) * 60;
Plugin.Instance.Configuration.PrePaddingSeconds = int.Parse(root.PrePadding, CultureInfo.InvariantCulture) * 60;
Plugin.Instance.Configuration.PostPaddingSeconds = root.PostPadding;
Plugin.Instance.Configuration.PrePaddingSeconds = root.PrePadding;
Plugin.Instance.Configuration.ShowRepeat = root.ShowNewInGuide;
Plugin.Instance.Configuration.BackendVersion = root.NextPvrVersion;
return true;
}
@ -35,7 +36,8 @@ public class SettingResponse
{
public string Version { get; set; }
public string NextPvrVersion { get; set; }
[JsonPropertyName("nextPVRVersion")]
public int NextPvrVersion { get; set; }
public string ReadableVersion { get; set; }
@ -59,9 +61,9 @@ public class SettingResponse
public string RecordingView { get; set; }
public string PrePadding { get; set; }
public int PrePadding { get; set; }
public string PostPadding { get; set; }
public int PostPadding { get; set; }
public bool ConfirmOnDelete { get; set; }

View File

@ -1,6 +1,4 @@
using Jellyfin.Plugin.NextPVR;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;

View File

@ -22,6 +22,20 @@
</label>
</div>
<div class="checkboxContainer">
<label>
<input is="emby-checkbox" type="checkbox" id="chkInProgress" />
<span>Force in-progress recordings</span>
</label>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="numPoll" required pattern="[0-9]*" min="0" max="600" step="10" label="Poll NextPVR Server"/>
<div class="fieldDescription">
Check to reload recording changes from the server in (seconds). Use 0 to disable polling.
</div>
</div>
<div class="selectContainer">
<select is="emby-select" id="selRecDefault" label="Series Recording Default">
<option value="1">New episodes on this channel</option>
@ -34,8 +48,9 @@
<div class="selectContainer">
<select is="emby-select" id="selRecTransport" label="Recording Protocol">
<option value="1">Streaming</option>
<option value="2">Filename</option>
<option value="1">Streaming</option>
<option value="3">Unauthenticated streaming</option>
</select>
</div>

View File

@ -2,6 +2,10 @@ const NextPvrConfigurationPage = {
pluginUniqueId: '9574ac10-bf23-49bc-949f-924f23cfa48f'
};
var authentication = "";
var transport;
var inprogress;
function loadGenres(config, page) {
if (config != null && config.GenreMappings) {
if (config.GenreMappings['GENREMOVIE'] != null) {
@ -21,23 +25,28 @@ function loadGenres(config, page) {
}
}
}
export default function (view) {
view.addEventListener('viewshow', function () {
export default function(view) {
view.addEventListener('viewshow', function() {
Dashboard.showLoadingMsg();
const page = this;
ApiClient.getPluginConfiguration(NextPvrConfigurationPage.pluginUniqueId).then(function(config) {
page.querySelector('#txtWebServiceUrl').value = config.WebServiceUrl || '';
page.querySelector('#txtPin').value = config.Pin || '';
page.querySelector('#numPoll').value = config.PollInterval;
page.querySelector('#chkDebugLogging').checked = config.EnableDebugLogging;
page.querySelector('#chkInProgress').checked = config.EnableInProgress;
page.querySelector('#chkNewEpisodes').checked = config.NewEpisodes;
page.querySelector('#selRecDefault').value = config.RecordingDefault;
page.querySelector('#selRecTransport').value = config.RecordingTransport;
loadGenres(config, page);
authentication = config.WebServiceUrl + config.Pin;
transport = config.RecordingTransport;
inprogress = config.EnableInProgress;
Dashboard.hideLoadingMsg();
});
});
view.querySelector('.nextpvrConfigurationForm').addEventListener('submit', function (e) {
view.querySelector('.nextpvrConfigurationForm').addEventListener('submit', function(e) {
Dashboard.showLoadingMsg();
const form = this;
@ -45,9 +54,24 @@ export default function (view) {
config.WebServiceUrl = form.querySelector('#txtWebServiceUrl').value;
config.Pin = form.querySelector('#txtPin').value;
config.EnableDebugLogging = form.querySelector('#chkDebugLogging').checked;
config.EnableInProgress = form.querySelector('#chkInProgress').checked;
config.NewEpisodes = form.querySelector('#chkNewEpisodes').checked;
config.RecordingDefault = form.querySelector('#selRecDefault').value;
config.RecordingTransport = form.querySelector('#selRecTransport').value;
config.PollInterval = form.querySelector('#numPoll').value;
if (authentication != config.WebServiceUrl + config.Pin) {
config.StoredSid = "";
config.CurrentWebServiceURL = "";
// Date will be updated;
var myJsDate = new Date();
config.RecordingModificationTime = myJsDate.toISOString();
} else if (transport != config.RecordingTransport || inprogress != config.EnableInProgress) {
var myJsDate = new Date();
config.RecordingModificationTime = myJsDate.toISOString();
}
authentication = config.WebServiceUrl + config.Pin;
transport = config.RecordingTransport;
inprogress = config.EnableInProgress;
// Copy over the genre mapping fields
config.GenreMappings = {
'GENREMOVIE': form.querySelector('#txtMovieGenre').value.split(','),
@ -63,4 +87,4 @@ export default function (view) {
// Disable default form submission
return false;
});
}
}