diff --git a/Directory.Build.props b/Directory.Build.props index 9257d94..49d976b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 9.0.0.0 - 9.0.0.0 - 9.0.0.0 + 10.0.0.0 + 10.0.0.0 + 10.0.0.0 diff --git a/Jellyfin.Plugin.NextPVR/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.NextPVR/Configuration/PluginConfiguration.cs index 34a3d08..516727d 100644 --- a/Jellyfin.Plugin.NextPVR/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.NextPVR/Configuration/PluginConfiguration.cs @@ -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>(); GenreMappings["GENRESPORT"] = new List() @@ -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; } /// /// Gets or sets the genre mappings, to map localised NextPVR genres, to Jellyfin categories. diff --git a/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj b/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj index 22c2d15..175004f 100644 --- a/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj +++ b/Jellyfin.Plugin.NextPVR/Jellyfin.Plugin.NextPVR.csproj @@ -7,6 +7,7 @@ true ../jellyfin.ruleset CA2227;CA1002;CA2007;CS1591 + 10.0.0.0 diff --git a/Jellyfin.Plugin.NextPVR/LiveTvService.cs b/Jellyfin.Plugin.NextPVR/LiveTvService.cs index 62eb7fc..931a053 100644 --- a/Jellyfin.Plugin.NextPVR/LiveTvService.cs +++ b/Jellyfin.Plugin.NextPVR/LiveTvService.cs @@ -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 _logger; private int _liveStreams; private DateTimeOffset _lastRecordingChange = DateTimeOffset.MinValue; - public LiveTvService(IHttpClientFactory httpClientFactory, ILogger logger) + private string baseUrl; + + public LiveTvService(IHttpClientFactory httpClientFactory, ILogger 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; } /// @@ -63,27 +72,43 @@ public class LiveTvService : ILiveTvService /// /// The cancellation token. /// A representing the asynchronous operation. - public async Task EnsureConnectionAsync(CancellationToken cancellationToken) + public async Task 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; } /// @@ -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 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); } /// - /// Create a new recording. + /// Create a new scheduled recording. /// /// The TimerInfo. /// The cancellationToken. @@ -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); } /// - /// Get the pending Recordings. + /// Get the pending Timers. /// /// The CancellationToken. /// A representing the asynchronous operation. public async Task> 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(); } /// @@ -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 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); diff --git a/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs b/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs index e20006a..5d6f8cb 100644 --- a/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs +++ b/Jellyfin.Plugin.NextPVR/RecordingsChannel.cs @@ -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 _logger; private readonly CancellationTokenSource _cancellationToken; + private readonly string _recordingCacheDirectory; + private static SemaphoreSlim _semaphore; + private Timer _updateTimer; private DateTimeOffset _lastUpdate = DateTimeOffset.FromUnixTimeSeconds(0); private IEnumerable _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 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(); + 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 GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken) + public async Task 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() }; - return Task.FromResult(result); + return result; } public async Task GetChannelItems(InternalChannelItemQuery query, Func 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 pluginItems = new List(); 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 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 GetRecordingGroups(InternalChannelItemQuery query, CancellationToken cancellationToken) { List pluginItems = new List(); - 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(); + } } diff --git a/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs index acc65bc..2d0d33d 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/RecordingResponse.cs @@ -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) { diff --git a/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs b/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs index b26bc0e..27b914f 100644 --- a/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs +++ b/Jellyfin.Plugin.NextPVR/Responses/SettingResponse.cs @@ -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(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; } diff --git a/Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs b/Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs index 88ce185..e22a11f 100644 --- a/Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs +++ b/Jellyfin.Plugin.NextPVR/ServiceRegistrator.cs @@ -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; diff --git a/Jellyfin.Plugin.NextPVR/Web/nextpvr.html b/Jellyfin.Plugin.NextPVR/Web/nextpvr.html index 77e6482..34e2de8 100644 --- a/Jellyfin.Plugin.NextPVR/Web/nextpvr.html +++ b/Jellyfin.Plugin.NextPVR/Web/nextpvr.html @@ -22,6 +22,20 @@ + + + + Force in-progress recordings + + + + + + + Check to reload recording changes from the server in (seconds). Use 0 to disable polling. + + + New episodes on this channel @@ -34,8 +48,9 @@ - Streaming Filename + Streaming + Unauthenticated streaming diff --git a/Jellyfin.Plugin.NextPVR/Web/nextpvr.js b/Jellyfin.Plugin.NextPVR/Web/nextpvr.js index 38e0cde..479622e 100644 --- a/Jellyfin.Plugin.NextPVR/Web/nextpvr.js +++ b/Jellyfin.Plugin.NextPVR/Web/nextpvr.js @@ -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; }); -} +} \ No newline at end of file