jellyfin-plugin-nextpvr/Jellyfin.Plugin.NextPVR/LiveTvService.cs

649 lines
29 KiB
C#
Raw Permalink Normal View History

2017-09-02 18:05:52 +00:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
2020-11-18 21:46:14 +00:00
using System.Net.Http;
2021-12-11 16:34:59 +00:00
using System.Security.Cryptography;
2017-09-02 18:05:52 +00:00
using System.Text;
2021-12-11 16:34:59 +00:00
using System.Text.Json;
2017-09-02 18:05:52 +00:00
using System.Threading;
using System.Threading.Tasks;
2021-12-11 16:34:59 +00:00
using Jellyfin.Plugin.NextPVR.Entities;
using Jellyfin.Plugin.NextPVR.Helpers;
using Jellyfin.Plugin.NextPVR.Responses;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
2021-12-11 16:34:59 +00:00
namespace Jellyfin.Plugin.NextPVR;
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Class LiveTvService.
/// </summary>
public class LiveTvService : ILiveTvService
2017-09-02 18:05:52 +00:00
{
2021-12-11 16:34:59 +00:00
private readonly IHttpClientFactory _httpClientFactory;
private readonly bool _enableIPv6;
2021-12-11 16:34:59 +00:00
private readonly ILogger<LiveTvService> _logger;
private int _liveStreams;
2024-06-14 13:17:26 +00:00
private string _baseUrl;
public LiveTvService(IHttpClientFactory httpClientFactory, ILogger<LiveTvService> logger, IConfigurationManager configuration)
2017-09-02 18:05:52 +00:00
{
_enableIPv6 = configuration.GetNetworkConfiguration().EnableIPv6;
2021-12-11 16:34:59 +00:00
_httpClientFactory = httpClientFactory;
_logger = logger;
LastUpdatedSidDateTime = DateTime.UtcNow;
2024-05-21 16:10:18 +00:00
Instance = this;
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2024-05-25 15:13:20 +00:00
public string Sid { get; set; }
public DateTime RecordingModificationTime { get; set; }
2017-09-02 18:05:52 +00:00
2024-05-21 16:10:18 +00:00
public static LiveTvService Instance { get; private set; }
2024-06-14 13:30:05 +00:00
public bool IsActive => Sid is not null;
2018-02-02 17:29:20 +00:00
public bool FlagRecordingChange { get; set; }
2021-12-11 16:34:59 +00:00
private DateTimeOffset LastUpdatedSidDateTime { get; set; }
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
public string Name => "Next Pvr";
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
public string HomePageUrl => "https://www.nextpvr.com/";
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Ensure that we are connected to the NextPvr server.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task<bool> EnsureConnectionAsync(CancellationToken cancellationToken)
2021-12-11 16:34:59 +00:00
{
var config = Plugin.Instance.Configuration;
2017-09-02 18:05:52 +00:00
{
if (!Uri.IsWellFormedUriString(config.WebServiceUrl, UriKind.Absolute))
2024-05-25 15:13:20 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogError("Web service URL must be configured");
throw new InvalidOperationException("NextPVR web service URL must be configured.");
2024-05-25 15:13:20 +00:00
}
if (string.IsNullOrEmpty(config.Pin))
{
2024-06-14 13:30:05 +00:00
_logger.LogError("PIN must be configured");
throw new InvalidOperationException("NextPVR PIN must be configured.");
2024-05-25 15:13:20 +00:00
}
if (string.IsNullOrEmpty(config.StoredSid))
2024-05-25 15:13:20 +00:00
{
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;
2024-06-14 13:17:26 +00:00
_logger.LogError(ex, "Error initiating session");
}
2024-05-25 15:13:20 +00:00
}
2017-09-02 18:05:52 +00:00
}
return IsActive;
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Initiate the nextPvr session.
/// </summary>
private async Task InitiateSession(CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start InitiateSession");
2024-06-14 13:17:26 +00:00
_baseUrl = Plugin.Instance.Configuration.CurrentWebServiceURL;
2021-12-11 16:34:59 +00:00
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
httpClient.Timeout = TimeSpan.FromSeconds(5);
bool updateConfiguration = false;
bool validConfiguration = false;
if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.StoredSid) && !string.IsNullOrEmpty(Plugin.Instance.Configuration.CurrentWebServiceURL) )
2024-05-25 15:13:20 +00:00
{
2024-06-14 13:17:26 +00:00
string request = $"{_baseUrl}/service?method=session.valid&device=jellyfin&sid={Plugin.Instance.Configuration.StoredSid}";
await using var stream = await httpClient.GetStreamAsync(request, cancellationToken).ConfigureAwait(false);
validConfiguration = await new InitializeResponse().LoggedIn(stream, _logger).ConfigureAwait(false);
2024-05-25 15:13:20 +00:00
}
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";
}
2017-09-02 18:05:52 +00:00
try
{
Uri uri = new Uri(Plugin.Instance.Configuration.WebServiceUrl);
var hosts = await Dns.GetHostEntryAsync(uri.Host, System.Net.Sockets.AddressFamily.InterNetwork, cancellationToken);
2024-06-14 13:30:05 +00:00
if (hosts is not null)
{
2024-06-14 13:17:26 +00:00
var host = hosts.AddressList.FirstOrDefault()?.ToString();
2024-06-14 13:30:05 +00:00
if (builder.Host != host && host is not null)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Changed host from {OldHost} to {NewHost}", builder.Host, host);
builder.Host = host;
}
}
}
catch (Exception ex)
{
2024-06-14 13:30:05 +00:00
_logger.LogError(ex, "Could not resolve {WebServiceUrl}", Plugin.Instance.Configuration.WebServiceUrl);
}
}
2017-09-02 18:05:52 +00:00
2024-06-14 13:17:26 +00:00
_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;
}
2017-09-02 18:05:52 +00:00
if (validConfiguration)
2021-12-11 16:34:59 +00:00
{
LastUpdatedSidDateTime = DateTimeOffset.UtcNow;
Sid = Plugin.Instance.Configuration.StoredSid;
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Session initiated");
_logger.LogInformation("Sid: {Sid}", Sid);
if (updateConfiguration)
{
2024-06-14 13:17:26 +00:00
Plugin.Instance.Configuration.CurrentWebServiceURL = _baseUrl;
Plugin.Instance.Configuration.RecordingModificationTime = DateTime.UtcNow;
Plugin.Instance.SaveConfiguration();
}
RecordingModificationTime = Plugin.Instance.Configuration.RecordingModificationTime;
2021-12-11 16:34:59 +00:00
await GetDefaultSettingsAsync(cancellationToken).ConfigureAwait(false);
Plugin.Instance.Configuration.GetEpisodeImage = await GetBackendSettingAsync("/Settings/General/ArtworkFromSchedulesDirect", cancellationToken).ConfigureAwait(false) == "true";
2017-09-02 18:05:52 +00:00
}
2021-12-11 16:34:59 +00:00
else
2018-02-02 22:06:08 +00:00
{
Sid = null;
2024-06-14 13:30:05 +00:00
_logger.LogError("PIN not accepted");
2021-12-11 16:34:59 +00:00
throw new UnauthorizedAccessException("NextPVR PIN not accepted");
2018-02-02 22:06:08 +00:00
}
2021-12-11 16:34:59 +00:00
}
2018-02-02 22:06:08 +00:00
2021-12-11 16:34:59 +00:00
private async Task<bool> Login(string sid, string salt, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start Login procedure for Sid: {Sid} & Salt: {Salt}", sid, salt);
2021-12-11 16:34:59 +00:00
var pin = Plugin.Instance.Configuration.Pin;
2024-06-14 13:30:05 +00:00
_logger.LogInformation("PIN: {Pin}", pin == "0000" ? pin : "Not default");
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
var strb = new StringBuilder();
var md5Result = GetMd5Hash(strb.Append(':').Append(GetMd5Hash(pin)).Append(':').Append(salt).ToString());
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
2024-06-14 13:17:26 +00:00
await using var stream = await httpClient.GetStreamAsync($"{_baseUrl}/service?method=session.login&md5={md5Result}&sid={sid}", cancellationToken);
2021-12-11 16:34:59 +00:00
{
return await new InitializeResponse().LoggedIn(stream, _logger).ConfigureAwait(false);
2017-09-02 18:05:52 +00:00
}
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
private string GetMd5Hash(string value)
{
#pragma warning disable CA5351
2024-05-21 16:10:18 +00:00
var hashValue = MD5.HashData(new UTF8Encoding().GetBytes(value));
2021-12-11 16:34:59 +00:00
#pragma warning restore CA5351
// Bit convertor return the byte to string as all caps hex values separated by "-"
return BitConverter.ToString(hashValue).Replace("-", string.Empty, StringComparison.Ordinal).ToLowerInvariant();
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Gets the channels async.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IEnumerable{ChannelInfo}}.</returns>
public async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start GetChannels Async, retrieve all channels");
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=channel.list&sid={Sid}", cancellationToken);
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
return await new ChannelResponse(Plugin.Instance.Configuration.WebServiceUrl).GetChannels(stream, _logger).ConfigureAwait(false);
}
2020-03-25 17:09:33 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Gets the Recordings async.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IEnumerable{RecordingInfo}}.</returns>
2024-06-14 13:30:05 +00:00
public async Task<IReadOnlyList<MyRecordingInfo>> GetAllRecordingsAsync(CancellationToken cancellationToken)
2021-12-11 16:34:59 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start GetRecordings Async, retrieve all 'Pending', 'Inprogress' and 'Completed' recordings ");
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=recording.list&filter=ready&sid={Sid}", cancellationToken);
return await new RecordingResponse(_baseUrl, _logger).GetRecordings(stream).ConfigureAwait(false);
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Delete the Recording async from the disk.
/// </summary>
/// <param name="recordingId">The recordingId.</param>
/// <param name="cancellationToken">The cancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start Delete Recording Async for recordingId: {RecordingId}", recordingId);
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=recording.delete&recording_id={recordingId}&sid={Sid}", cancellationToken);
2020-03-25 17:09:33 +00:00
2021-12-11 16:34:59 +00:00
bool? error = await new CancelDeleteRecordingResponse().RecordingError(stream, _logger).ConfigureAwait(false);
2020-11-18 21:46:14 +00:00
2024-06-14 13:30:05 +00:00
if (error is null or true)
2021-12-11 16:34:59 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogError("Failed to delete the recording for recordingId: {RecordingId}", recordingId);
2021-12-11 16:34:59 +00:00
throw new JsonException($"Failed to delete the recording for recordingId: {recordingId}");
2017-09-02 18:05:52 +00:00
}
else
{
FlagRecordingChange = true;
}
2017-09-02 18:05:52 +00:00
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Deleted Recording with recordingId: {RecordingId}", recordingId);
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Cancel pending scheduled Recording.
/// </summary>
/// <param name="timerId">The timerId.</param>
/// <param name="cancellationToken">The cancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start Cancel Recording Async for recordingId: {TimerId}", timerId);
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=recording.delete&recording_id={timerId}&sid={Sid}", cancellationToken);
2020-11-18 21:46:14 +00:00
2021-12-11 16:34:59 +00:00
bool? error = await new CancelDeleteRecordingResponse().RecordingError(stream, _logger).ConfigureAwait(false);
2020-11-18 21:46:14 +00:00
2024-06-14 13:30:05 +00:00
if (error is null or true)
2021-12-11 16:34:59 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogError("Failed to cancel the recording for recordingId: {TimerId}", timerId);
2021-12-11 16:34:59 +00:00
throw new JsonException($"Failed to cancel the recording for recordingId: {timerId}");
2017-09-02 18:05:52 +00:00
}
else
{
FlagRecordingChange = true;
}
2017-09-02 18:05:52 +00:00
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Cancelled Recording for recordingId: {TimerId}", timerId);
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Create a new scheduled recording.
2021-12-11 16:34:59 +00:00
/// </summary>
/// <param name="info">The TimerInfo.</param>
/// <param name="cancellationToken">The cancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start CreateTimer Async for ChannelId: {ChannelId} & Name: {Name}", info.ChannelId, info.Name);
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2024-06-14 13:30:05 +00:00
UtilsHelper.DebugInformation(_logger, $"TimerSettings CreateTimer: {info.ProgramId} for ChannelId: {info.ChannelId} & Name: {info.Name}");
2021-12-11 16:34:59 +00:00
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync(
string.Format(
CultureInfo.InvariantCulture,
"{0}/service?method=recording.save&sid={1}&event_id={2}&pre_padding={3}&post_padding={4}",
2024-06-14 13:17:26 +00:00
_baseUrl,
2021-12-11 16:34:59 +00:00
Sid,
int.Parse(info.ProgramId, CultureInfo.InvariantCulture),
info.PrePaddingSeconds / 60,
info.PostPaddingSeconds / 60),
cancellationToken);
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
bool? error = await new CancelDeleteRecordingResponse().RecordingError(stream, _logger).ConfigureAwait(false);
2024-06-14 13:30:05 +00:00
if (error is null or true)
2017-09-02 18:05:52 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogError("Failed to create the timer with programId: {ProgramId}", info.ProgramId);
2021-12-11 16:34:59 +00:00
throw new JsonException($"Failed to create the timer with programId: {info.ProgramId}");
2017-09-02 18:05:52 +00:00
}
else if (info.StartDate <= DateTime.UtcNow)
{
FlagRecordingChange = true;
}
2017-09-02 18:05:52 +00:00
2024-06-14 13:30:05 +00:00
_logger.LogError("CreateTimer async for programId: {ProgramId}", info.ProgramId);
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Get the pending Timers.
2021-12-11 16:34:59 +00:00
/// </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)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start GetTimer Async, retrieve the 'Pending' recordings");
if (await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false))
{
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=recording.list&filter=pending&sid={Sid}", cancellationToken);
2017-09-02 18:05:52 +00:00
2024-06-14 13:17:26 +00:00
return await new RecordingResponse(_baseUrl, _logger).GetTimers(stream).ConfigureAwait(false);
}
return new List<TimerInfo>();
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Get the recurrent recordings.
/// </summary>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start GetSeriesTimer Async, retrieve the recurring recordings");
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=recording.recurring.list&sid={Sid}", cancellationToken);
2021-12-11 16:34:59 +00:00
return await new RecurringResponse(_logger).GetSeriesTimers(stream).ConfigureAwait(false);
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Create a recurrent recording.
/// </summary>
/// <param name="info">The recurring program info.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start CreateSeriesTimer Async for ChannelId: {ChannelId} & Name: {Name}", info.ChannelId, info.Name);
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2024-06-14 13:17:26 +00:00
var url = $"{_baseUrl}/service?method=recording.recurring.save&sid={Sid}&pre_padding={info.PrePaddingSeconds / 60}&post_padding={info.PostPaddingSeconds / 60}&keep={info.KeepUpTo}";
2021-12-11 16:34:59 +00:00
int recurringType = int.Parse(Plugin.Instance.Configuration.RecordingDefault, CultureInfo.InvariantCulture);
if (recurringType == 99)
{
2021-12-11 16:34:59 +00:00
url += string.Format(CultureInfo.InvariantCulture, "&name={0}&keyword=title+like+'{0}'", Uri.EscapeDataString(info.Name.Replace("'", "''", StringComparison.Ordinal)));
}
else
{
url += $"&event_id={info.ProgramId}&recurring_type={recurringType}";
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
if (info.RecordNewOnly || Plugin.Instance.Configuration.NewEpisodes)
{
url += "&only_new=true";
}
2020-11-18 21:46:14 +00:00
2024-06-14 13:30:05 +00:00
if (recurringType is 3 or 4)
2021-12-11 16:34:59 +00:00
{
url += "&timeslot=true";
2017-09-02 18:05:52 +00:00
}
2021-12-11 16:34:59 +00:00
await CreateUpdateSeriesTimerAsync(info, url, cancellationToken);
}
2024-06-14 13:30:05 +00:00
private async Task CreateUpdateSeriesTimerAsync(SeriesTimerInfo info, string url, CancellationToken cancellationToken)
2021-12-11 16:34:59 +00:00
{
2024-06-14 13:30:05 +00:00
UtilsHelper.DebugInformation(_logger, $"TimerSettings CreateSeriesTimerAsync: {info.ProgramId} for ChannelId: {info.ChannelId} & Name: {info.Name}");
2021-12-11 16:34:59 +00:00
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync(url, cancellationToken);
bool? error = await new CancelDeleteRecordingResponse().RecordingError(stream, _logger).ConfigureAwait(false);
2024-06-14 13:30:05 +00:00
if (error is null or true)
2017-09-02 18:05:52 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogError("Failed to create or update the timer with Recurring ID: {TimerInfoId}", info.Id);
2021-12-11 16:34:59 +00:00
throw new JsonException($"Failed to create or update the timer with Recurring ID: {info.Id}");
}
2017-09-02 18:05:52 +00:00
2024-06-14 13:30:05 +00:00
_logger.LogInformation("CreateUpdateSeriesTimer async for Program ID: {ProgramId} Recurring ID {TimerInfoId}", info.ProgramId, info.Id);
2021-12-11 16:34:59 +00:00
}
2021-12-11 16:34:59 +00:00
/// <summary>
/// Update the series Timer.
/// </summary>
/// <param name="info">The series program info.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start UpdateSeriesTimer Async for ChannelId: {ChannelId} & Name: {Name}", info.ChannelId, info.Name);
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2017-09-02 18:05:52 +00:00
2024-06-14 13:17:26 +00:00
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}";
2021-12-11 16:34:59 +00:00
int recurringType = 2;
if (info.RecordAnyChannel)
{
url += string.Format(CultureInfo.InvariantCulture, "&name={0}&keyword=title+like+'{0}'", Uri.EscapeDataString(info.Name.Replace("'", "''", StringComparison.Ordinal)));
}
else
{
if (info.RecordAnyTime)
2017-09-02 18:05:52 +00:00
{
2021-12-11 16:34:59 +00:00
if (info.RecordNewOnly)
{
recurringType = 1;
}
2017-09-02 18:05:52 +00:00
}
else
2017-09-02 18:05:52 +00:00
{
2024-06-14 13:30:05 +00:00
recurringType = info.Days.Count == 7 ? 4 : 3;
2017-09-02 18:05:52 +00:00
}
2021-12-11 16:34:59 +00:00
url += $"&recurring_type={recurringType}";
2017-09-02 18:05:52 +00:00
}
2021-12-11 16:34:59 +00:00
if (info.RecordNewOnly)
2017-09-02 18:05:52 +00:00
{
2021-12-11 16:34:59 +00:00
url += "&only_new=true";
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
await CreateUpdateSeriesTimerAsync(info, url, cancellationToken).ConfigureAwait(false);
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Update a single Timer.
/// </summary>
/// <param name="updatedTimer">The program info.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start UpdateTimer Async for ChannelId: {ChannelId} & Name: {Name}", updatedTimer.ChannelId, updatedTimer.Name);
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.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);
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
bool? error = await new CancelDeleteRecordingResponse().RecordingError(stream, _logger).ConfigureAwait(false);
2024-06-14 13:30:05 +00:00
if (error is null or true)
2017-09-02 18:05:52 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogError("Failed to update the timer with ID: {Id}", updatedTimer.Id);
2021-12-11 16:34:59 +00:00
throw new JsonException($"Failed to update the timer with ID: {updatedTimer.Id}");
}
2017-09-02 18:05:52 +00:00
2024-06-14 13:30:05 +00:00
_logger.LogInformation("UpdateTimer async for Program ID: {ProgramId} ID {Id}", updatedTimer.ProgramId, updatedTimer.Id);
2021-12-11 16:34:59 +00:00
}
2020-03-25 17:09:33 +00:00
2021-12-11 16:34:59 +00:00
/// <summary>
/// Cancel the Series Timer.
/// </summary>
/// <param name="timerId">The Timer Id.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start Cancel SeriesRecording Async for recordingId: {TimerId}", timerId);
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=recording.recurring.delete&recurring_id={timerId}&sid={Sid}", cancellationToken);
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
bool? error = await new CancelDeleteRecordingResponse().RecordingError(stream, _logger).ConfigureAwait(false);
2017-09-02 18:05:52 +00:00
2024-06-14 13:30:05 +00:00
if (error is null or true)
2017-09-02 18:05:52 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogError("Failed to cancel the recording with recordingId: {TimerId}", timerId);
2021-12-11 16:34:59 +00:00
throw new JsonException($"Failed to cancel the recording with recordingId: {timerId}");
2017-09-02 18:05:52 +00:00
}
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Cancelled Recording for recordingId: {TimerId}", timerId);
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
2021-12-11 16:34:59 +00:00
{
var source = await GetChannelStream(channelId, string.Empty, cancellationToken);
2024-06-14 13:30:05 +00:00
return [source];
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start ChannelStream");
EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2021-12-11 16:34:59 +00:00
_liveStreams++;
string sidParameter = null;
if (Plugin.Instance.Configuration.RecordingTransport != 3)
{
sidParameter = $"&sid={Sid}";
}
2024-06-14 13:17:26 +00:00
string streamUrl = $"{_baseUrl}/live?channeloid={channelId}&client=jellyfin.{_liveStreams.ToString(CultureInfo.InvariantCulture)}{sidParameter}";
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Streaming {Url}", streamUrl);
2021-12-11 16:34:59 +00:00
var mediaSourceInfo = new MediaSourceInfo
{
Id = _liveStreams.ToString(CultureInfo.InvariantCulture),
Path = streamUrl,
Protocol = MediaProtocol.Http,
RequiresOpening = true,
2021-12-11 16:34:59 +00:00
MediaStreams = new List<MediaStream>
2017-09-02 18:05:52 +00:00
{
2021-12-11 16:34:59 +00:00
new MediaStream
2017-09-02 18:05:52 +00:00
{
2021-12-11 16:34:59 +00:00
Type = MediaStreamType.Video,
// IsInterlaced = true,
2021-12-11 16:34:59 +00:00
// Set the index to -1 because we don't know the exact index of the video stream within the container
Index = -1,
},
new MediaStream
2017-09-02 18:05:52 +00:00
{
2021-12-11 16:34:59 +00:00
Type = MediaStreamType.Audio,
// Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1
}
},
Container = "mpegts",
SupportsProbing = true
};
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
return Task.FromResult(mediaSourceInfo);
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
public Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Closing {Id}", id);
2021-12-11 16:34:59 +00:00
return Task.CompletedTask;
}
2021-12-11 16:34:59 +00:00
public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
{
SeriesTimerInfo defaultSettings = new SeriesTimerInfo
{
2021-12-11 16:34:59 +00:00
PrePaddingSeconds = Plugin.Instance.Configuration.PrePaddingSeconds,
PostPaddingSeconds = Plugin.Instance.Configuration.PostPaddingSeconds
};
return Task.FromResult(defaultSettings);
}
2020-11-18 21:46:14 +00:00
2024-06-14 13:30:05 +00:00
private async Task GetDefaultSettingsAsync(CancellationToken cancellationToken)
2021-12-11 16:34:59 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start GetDefaultSettings Async");
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=setting.list&sid={Sid}", cancellationToken);
2024-06-14 13:30:05 +00:00
await new SettingResponse().GetDefaultSettings(stream, _logger).ConfigureAwait(false);
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("Start GetPrograms Async, retrieve all Programs");
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.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);
2021-12-11 16:34:59 +00:00
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
public async Task<DateTimeOffset> GetLastUpdate(CancellationToken cancellationToken)
{
2024-06-14 13:30:05 +00:00
_logger.LogDebug("GetLastUpdateTime");
2021-12-11 16:34:59 +00:00
DateTimeOffset retTime = DateTimeOffset.FromUnixTimeSeconds(0);
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
try
2017-09-02 18:05:52 +00:00
{
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
httpClient.Timeout = TimeSpan.FromSeconds(5);
2024-06-14 13:17:26 +00:00
var stream = await httpClient.GetStreamAsync($"{_baseUrl}/service?method=recording.lastupdated&ignore_resume=true&sid={Sid}", cancellationToken);
2021-12-11 16:34:59 +00:00
retTime = await new LastUpdateResponse().GetUpdateTime(stream, _logger).ConfigureAwait(false);
if (retTime == DateTimeOffset.FromUnixTimeSeconds(0))
2017-09-02 18:05:52 +00:00
{
2021-12-11 16:34:59 +00:00
LastUpdatedSidDateTime = DateTimeOffset.MinValue;
2017-09-02 18:05:52 +00:00
}
2021-12-11 16:34:59 +00:00
else if (LastUpdatedSidDateTime != DateTimeOffset.MinValue)
2017-09-02 18:05:52 +00:00
{
LastUpdatedSidDateTime = DateTimeOffset.UtcNow;
2017-09-02 18:05:52 +00:00
}
2024-06-14 13:30:05 +00:00
UtilsHelper.DebugInformation(_logger, $"GetLastUpdateTime {retTime.ToUnixTimeSeconds()}");
2017-09-02 18:05:52 +00:00
}
2021-12-11 16:34:59 +00:00
catch (HttpRequestException)
{
2021-12-11 16:34:59 +00:00
LastUpdatedSidDateTime = DateTimeOffset.MinValue;
_logger.LogWarning("Could not connect to servier");
Sid = null;
}
2021-12-11 16:34:59 +00:00
return retTime;
}
2024-06-14 13:30:05 +00:00
private async Task<string> GetBackendSettingAsync(string key, CancellationToken cancellationToken)
2021-12-11 16:34:59 +00:00
{
2024-06-14 13:30:05 +00:00
_logger.LogInformation("GetBackendSetting");
2021-12-11 16:34:59 +00:00
await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
2024-06-14 13:17:26 +00:00
.GetStreamAsync($"{_baseUrl}/service?method=setting.get&key={key}&sid={Sid}", cancellationToken);
2021-12-11 16:34:59 +00:00
return await new SettingResponse().GetSetting(stream, _logger).ConfigureAwait(false);
}
2017-09-02 18:05:52 +00:00
2021-12-11 16:34:59 +00:00
public Task ResetTuner(string id, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
2017-09-02 18:05:52 +00:00
}