mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-28 11:28:27 +01:00
Handle 5002, 5003 and add caches
This commit is contained in:
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -18,8 +17,6 @@ using Jellyfin.Data.Enums;
|
|||||||
using Jellyfin.Database.Implementations.Enums;
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
@@ -49,12 +46,11 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
private readonly IListingsManager _listingsManager;
|
private readonly IListingsManager _listingsManager;
|
||||||
private readonly IRecordingsManager _recordingsManager;
|
private readonly IRecordingsManager _recordingsManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
private readonly IDtoService _dtoService;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly IConfigurationManager _configurationManager;
|
|
||||||
private readonly ITranscodeManager _transcodeManager;
|
private readonly ITranscodeManager _transcodeManager;
|
||||||
|
private readonly ISchedulesDirectService _schedulesDirectService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
|
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
|
||||||
@@ -65,12 +61,11 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <param name="listingsManager">Instance of the <see cref="IListingsManager"/> interface.</param>
|
/// <param name="listingsManager">Instance of the <see cref="IListingsManager"/> interface.</param>
|
||||||
/// <param name="recordingsManager">Instance of the <see cref="IRecordingsManager"/> interface.</param>
|
/// <param name="recordingsManager">Instance of the <see cref="IRecordingsManager"/> interface.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
|
||||||
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
|
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
|
||||||
|
/// <param name="schedulesDirectService">Instance of the <see cref="ISchedulesDirectService"/> interface.</param>
|
||||||
public LiveTvController(
|
public LiveTvController(
|
||||||
ILiveTvManager liveTvManager,
|
ILiveTvManager liveTvManager,
|
||||||
IGuideManager guideManager,
|
IGuideManager guideManager,
|
||||||
@@ -78,12 +73,11 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
IListingsManager listingsManager,
|
IListingsManager listingsManager,
|
||||||
IRecordingsManager recordingsManager,
|
IRecordingsManager recordingsManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IHttpClientFactory httpClientFactory,
|
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IDtoService dtoService,
|
IDtoService dtoService,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IConfigurationManager configurationManager,
|
ITranscodeManager transcodeManager,
|
||||||
ITranscodeManager transcodeManager)
|
ISchedulesDirectService schedulesDirectService)
|
||||||
{
|
{
|
||||||
_liveTvManager = liveTvManager;
|
_liveTvManager = liveTvManager;
|
||||||
_guideManager = guideManager;
|
_guideManager = guideManager;
|
||||||
@@ -91,12 +85,11 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
_listingsManager = listingsManager;
|
_listingsManager = listingsManager;
|
||||||
_recordingsManager = recordingsManager;
|
_recordingsManager = recordingsManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_httpClientFactory = httpClientFactory;
|
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_dtoService = dtoService;
|
_dtoService = dtoService;
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_configurationManager = configurationManager;
|
|
||||||
_transcodeManager = transcodeManager;
|
_transcodeManager = transcodeManager;
|
||||||
|
_schedulesDirectService = schedulesDirectService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -344,20 +337,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[Obsolete("This endpoint is obsolete.")]
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
|
public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
|
||||||
[FromQuery] string? channelId,
|
[FromQuery] string? channelId,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
@@ -387,7 +366,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[Obsolete("This endpoint is obsolete.")]
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
|
public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
return new QueryResult<BaseItemDto>();
|
return new QueryResult<BaseItemDto>();
|
||||||
@@ -832,7 +810,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[HttpPost("Timers/{timerId}")]
|
[HttpPost("Timers/{timerId}")]
|
||||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
|
||||||
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
|
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
|
||||||
{
|
{
|
||||||
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||||
@@ -922,7 +899,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[HttpPost("SeriesTimers/{timerId}")]
|
[HttpPost("SeriesTimers/{timerId}")]
|
||||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
|
||||||
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||||
{
|
{
|
||||||
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||||
@@ -1083,13 +1059,8 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||||
{
|
{
|
||||||
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
var bytes = await _schedulesDirectService.GetAvailableCountries(CancellationToken.None).ConfigureAwait(false);
|
||||||
// https://json.schedulesdirect.org/20141201/available/countries
|
return File(bytes, MediaTypeNames.Application.Json);
|
||||||
// Can't dispose the response as it's required up the call chain.
|
|
||||||
var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
17
MediaBrowser.Controller/LiveTv/ISchedulesDirectService.cs
Normal file
17
MediaBrowser.Controller/LiveTv/ISchedulesDirectService.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.LiveTv;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides Schedules Direct specific operations.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISchedulesDirectService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the available countries from the Schedules Direct API, using a file cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>The raw JSON response bytes.</returns>
|
||||||
|
Task<byte[]> GetAvailableCountries(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -40,7 +40,9 @@ public static class LiveTvServiceCollectionExtensions
|
|||||||
services.AddSingleton<ILiveTvService, DefaultLiveTvService>();
|
services.AddSingleton<ILiveTvService, DefaultLiveTvService>();
|
||||||
services.AddSingleton<ITunerHost, HdHomerunHost>();
|
services.AddSingleton<ITunerHost, HdHomerunHost>();
|
||||||
services.AddSingleton<ITunerHost, M3UTunerHost>();
|
services.AddSingleton<ITunerHost, M3UTunerHost>();
|
||||||
services.AddSingleton<IListingsProvider, SchedulesDirect>();
|
services.AddSingleton<SchedulesDirect>();
|
||||||
|
services.AddSingleton<IListingsProvider>(s => s.GetRequiredService<SchedulesDirect>());
|
||||||
|
services.AddSingleton<ISchedulesDirectService>(s => s.GetRequiredService<SchedulesDirect>());
|
||||||
services.AddSingleton<IListingsProvider, XmlTvListingsProvider>();
|
services.AddSingleton<IListingsProvider, XmlTvListingsProvider>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ public class GuideManager : IGuideManager
|
|||||||
private readonly IRecordingsManager _recordingsManager;
|
private readonly IRecordingsManager _recordingsManager;
|
||||||
private readonly LiveTvDtoService _tvDtoService;
|
private readonly LiveTvDtoService _tvDtoService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UTC date when the SD image download limit was hit. Cleared after 00:00 UTC rollover.
|
||||||
|
/// </summary>
|
||||||
|
private DateTime? _sdImageLimitHitDate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Amount of days images are pre-cached from external sources.
|
/// Amount of days images are pre-cached from external sources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -721,6 +726,20 @@ public class GuideManager : IGuideManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsSdImageLimitActive()
|
||||||
|
{
|
||||||
|
// The SD image counter resets daily at 00:00 UTC.
|
||||||
|
// If we recorded a limit hit on a previous UTC date, clear it.
|
||||||
|
var hitDate = _sdImageLimitHitDate;
|
||||||
|
if (hitDate.HasValue && hitDate.Value.Date < DateTime.UtcNow.Date)
|
||||||
|
{
|
||||||
|
_sdImageLimitHitDate = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hitDate.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
|
private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
|
||||||
{
|
{
|
||||||
await Parallel.ForEachAsync(
|
await Parallel.ForEachAsync(
|
||||||
@@ -738,19 +757,39 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imageInfo = program.ImageInfos[i];
|
var imageInfo = program.ImageInfos[i];
|
||||||
if (!imageInfo.IsLocalFile)
|
if (imageInfo.IsLocalFile)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
|
continue;
|
||||||
try
|
}
|
||||||
|
|
||||||
|
// Skip SD downloads once the daily limit has been hit.
|
||||||
|
if (imageInfo.Path.Contains("schedulesdirect", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& IsSdImageLimitActive())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
|
||||||
|
program,
|
||||||
|
imageInfo,
|
||||||
|
imageIndex: 0,
|
||||||
|
removeOnFailure: false)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (imageInfo.Path.Contains("schedulesdirect", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !_sdImageLimitHitDate.HasValue)
|
||||||
{
|
{
|
||||||
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
|
_sdImageLimitHitDate = DateTime.UtcNow;
|
||||||
program,
|
_logger.LogWarning(
|
||||||
imageInfo,
|
"Schedules Direct image download failed for {Url}. Daily download limit may have been reached (resets at 00:00 UTC). Skipping remaining SD images until reset",
|
||||||
imageIndex: 0,
|
imageInfo.Path);
|
||||||
removeOnFailure: false)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else if (!imageInfo.Path.Contains("schedulesdirect", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
|
_logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -21,6 +22,7 @@ using Jellyfin.Extensions;
|
|||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using Jellyfin.LiveTv.Guide;
|
using Jellyfin.LiveTv.Guide;
|
||||||
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
@@ -31,12 +33,14 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Jellyfin.LiveTv.Listings
|
namespace Jellyfin.LiveTv.Listings
|
||||||
{
|
{
|
||||||
public class SchedulesDirect : IListingsProvider, IDisposable
|
public class SchedulesDirect : IListingsProvider, ISchedulesDirectService, IDisposable
|
||||||
{
|
{
|
||||||
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
|
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
|
||||||
|
private const int CountryCacheDays = 7;
|
||||||
|
|
||||||
private readonly ILogger<SchedulesDirect> _logger;
|
private readonly ILogger<SchedulesDirect> _logger;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
|
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
|
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
|
||||||
@@ -45,17 +49,25 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
private bool _accountError;
|
private bool _accountError;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
private byte[] _countriesCache;
|
||||||
|
private DateTime? _dailyLimitHitDate;
|
||||||
|
|
||||||
public SchedulesDirect(
|
public SchedulesDirect(
|
||||||
ILogger<SchedulesDirect> logger,
|
ILogger<SchedulesDirect> logger,
|
||||||
IHttpClientFactory httpClientFactory)
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IApplicationPaths appPaths)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_appPaths = appPaths;
|
||||||
|
_dailyLimitHitDate = LoadDailyLimitHitDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "Schedules Direct";
|
public string Name => "Schedules Direct";
|
||||||
|
|
||||||
|
private string DailyLimitFilePath => Path.Combine(_appPaths.CachePath, "sd-daily-limit.txt");
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Type => nameof(SchedulesDirect);
|
public string Type => nameof(SchedulesDirect);
|
||||||
|
|
||||||
@@ -553,6 +565,19 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Daily usage limit hit (e.g. 5003) — wait until the SD counter resets at 00:00 UTC.
|
||||||
|
if (_dailyLimitHitDate.HasValue)
|
||||||
|
{
|
||||||
|
if (_dailyLimitHitDate.Value.Date < DateTime.UtcNow.Date)
|
||||||
|
{
|
||||||
|
ClearDailyLimitHitDate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid hammering SD after transient login failures (e.g. max attempts / temporary lockout)
|
// Avoid hammering SD after transient login failures (e.g. max attempts / temporary lockout)
|
||||||
if ((DateTime.UtcNow - _lastErrorResponse).TotalMinutes < 30)
|
if ((DateTime.UtcNow - _lastErrorResponse).TotalMinutes < 30)
|
||||||
{
|
{
|
||||||
@@ -662,6 +687,13 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
_tokens.Clear();
|
_tokens.Clear();
|
||||||
_lastErrorResponse = DateTime.UtcNow;
|
_lastErrorResponse = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
else if (sdCode is 5002 or 5003)
|
||||||
|
{
|
||||||
|
// Daily usage limits — stop requests until SD resets at 00:00 UTC.
|
||||||
|
// 5002=max image downloads
|
||||||
|
// 5003=max schedule/metadata requests
|
||||||
|
SetDailyLimitHitDate();
|
||||||
|
}
|
||||||
else if (enableRetry
|
else if (enableRetry
|
||||||
&& (int)response.StatusCode < 500
|
&& (int)response.StatusCode < 500
|
||||||
&& (sdCode == 4006 || (response.StatusCode == HttpStatusCode.Forbidden && sdCode is null)))
|
&& (sdCode == 4006 || (response.StatusCode == HttpStatusCode.Forbidden && sdCode is null)))
|
||||||
@@ -762,6 +794,107 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<byte[]> GetAvailableCountries(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_countriesCache is not null)
|
||||||
|
{
|
||||||
|
return _countriesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachePath = Path.Combine(_appPaths.CachePath, "sd-countries.json");
|
||||||
|
|
||||||
|
if (File.Exists(cachePath)
|
||||||
|
&& DateTime.UtcNow - File.GetLastWriteTimeUtc(cachePath) < TimeSpan.FromDays(CountryCacheDays))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_countriesCache = await File.ReadAllBytesAsync(cachePath, cancellationToken).ConfigureAwait(false);
|
||||||
|
return _countriesCache;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Corrupt or unreadable — delete and re-fetch.
|
||||||
|
TryDeleteFile(cachePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||||
|
using var response = await client.GetAsync(new Uri(ApiUrl + "/available/countries"), cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
|
||||||
|
await File.WriteAllBytesAsync(cachePath, bytes, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_countriesCache = bytes;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime? LoadDailyLimitHitDate()
|
||||||
|
{
|
||||||
|
var path = DailyLimitFilePath;
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText(path).Trim();
|
||||||
|
if (DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var date))
|
||||||
|
{
|
||||||
|
if (date.Date < DateTime.UtcNow.Date)
|
||||||
|
{
|
||||||
|
// Expired — clean up.
|
||||||
|
File.Delete(path);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Corrupt or unreadable — delete and reset.
|
||||||
|
TryDeleteFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDeleteFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Best effort.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDailyLimitHitDate()
|
||||||
|
{
|
||||||
|
_dailyLimitHitDate = DateTime.UtcNow;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(DailyLimitFilePath)!);
|
||||||
|
File.WriteAllText(DailyLimitFilePath, DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to persist SD daily limit hit date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearDailyLimitHitDate()
|
||||||
|
{
|
||||||
|
_dailyLimitHitDate = null;
|
||||||
|
TryDeleteFile(DailyLimitFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
|
public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
|
||||||
{
|
{
|
||||||
if (validateLogin)
|
if (validateLogin)
|
||||||
|
|||||||
Reference in New Issue
Block a user