mirror of
https://github.com/jellyfin/jellyfin-plugin-nextpvr.git
synced 2024-11-23 05:59:41 +00:00
Recording enhancements
Enhanced recording security , configuration and polling, in-progress playback, allow client direct play.
This commit is contained in:
parent
089b919e54
commit
cccea1622f
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user