Handle 5002, 5003 and add caches

This commit is contained in:
Shadowghost
2026-02-22 11:14:15 +01:00
parent d156e04c9a
commit 27396bffc6
5 changed files with 211 additions and 49 deletions

View File

@@ -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>

View 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);
}

View File

@@ -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>();
} }
} }

View File

@@ -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);
} }

View File

@@ -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)