diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-02-20 14:58:12 +0100 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-02-20 14:58:12 +0100 |
| commit | b0eec00e1cda109e5c6720f054932993108f0549 (patch) | |
| tree | 2fbfb7103bf1cf0595057c3b3cc8b982249b22fe /src | |
| parent | e49d71707c5f9f46fca373922a1ac1893cfc6ad5 (diff) | |
Properly handle SD internal error codes
Diffstat (limited to 'src')
| -rw-r--r-- | src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs | 99 |
1 files changed, 74 insertions, 25 deletions
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs index 939fd0f66d..2ca42c89ef 100644 --- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs @@ -42,6 +42,7 @@ namespace Jellyfin.LiveTv.Listings private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private DateTime _lastErrorResponse; + private bool _accountError; private bool _disposed = false; public SchedulesDirect( @@ -546,7 +547,13 @@ namespace Jellyfin.LiveTv.Listings return null; } - // Avoid hammering SD + // Permanent account error — SD is disabled for this server lifetime. + if (_accountError) + { + return null; + } + + // Avoid hammering SD after transient login failures (e.g. max attempts / temporary lockout) if ((DateTime.UtcNow - _lastErrorResponse).TotalMinutes < 30) { return null; @@ -579,7 +586,13 @@ namespace Jellyfin.LiveTv.Listings } catch (HttpRequestException ex) { - if (ex.StatusCode.HasValue && (int)ex.StatusCode.Value >= 400 && (int)ex.StatusCode.Value < 500) + // For 4xx errors not already handled by Request<T>'s SD code logic + // (e.g. unparseable response from the /token endpoint), apply a + // temporary backoff to avoid hammering SD. + if (!_accountError + && ex.StatusCode.HasValue + && (int)ex.StatusCode.Value >= 400 + && (int)ex.StatusCode.Value < 500) { _tokens.Clear(); _lastErrorResponse = DateTime.UtcNow; @@ -605,27 +618,70 @@ namespace Jellyfin.LiveTv.Listings return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken).ConfigureAwait(false); } - if (!enableRetry || (int)response.StatusCode >= 500) + var responseBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + // Try to extract the Schedules Direct error code from the response body. + int? sdCode = null; + try { - _logger.LogError( - "Request to {Url} failed with response {Response}", - message.RequestUri, - await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)); + using var doc = JsonDocument.Parse(responseBody); + if (doc.RootElement.TryGetProperty("code", out var codeProp) && codeProp.TryGetInt32(out var parsedCode)) + { + sdCode = parsedCode; + } + } + catch (JsonException) + { + // Response body is not valid JSON; sdCode stays null. + } + + _logger.LogError( + "Request to {Url} failed with HTTP {StatusCode}, SD code {SdCode}: {Response}", + message.RequestUri, + (int)response.StatusCode, + sdCode?.ToString(CultureInfo.InvariantCulture) ?? "N/A", + responseBody); - throw new HttpRequestException( - string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase), - null, - response.StatusCode); + if (sdCode is 4001 or 4003 or 4004 or 4005 or 4008) + { + // Permanent account errors — disable SD for this server lifetime. + // 4001=invalid user + // 4003=invalid hash + // 4004=account locked/disabled + // 4005=account expired + // 4008=password required + _logger.LogError("Schedules Direct account error (code {SdCode}). Disabling SD until server restart", sdCode); + _tokens.Clear(); + _accountError = true; + } + else if (sdCode is 4009 or 4010) + { + // Transient login errors — back off for 30 minutes, then allow retry. + // 4009=max login attempts + // 4010=temporary lockout + _tokens.Clear(); + _lastErrorResponse = DateTime.UtcNow; } + else if (enableRetry + && (int)response.StatusCode < 500 + && (sdCode == 4006 || (response.StatusCode == HttpStatusCode.Forbidden && sdCode is null))) + { + // 4006 = token expired — clear tokens and retry with a fresh token. + // Also retry on 403 with no parseable SD code (legacy/unexpected auth failure). + _tokens.Clear(); + using var retryMessage = new HttpRequestMessage(message.Method, message.RequestUri); + retryMessage.Content = message.Content; + retryMessage.Headers.TryAddWithoutValidation( + "token", + await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); - _tokens.Clear(); - using var retryMessage = new HttpRequestMessage(message.Method, message.RequestUri); - retryMessage.Content = message.Content; - retryMessage.Headers.TryAddWithoutValidation( - "token", - await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); + return await Request<T>(retryMessage, false, providerInfo, cancellationToken).ConfigureAwait(false); + } - return await Request<T>(retryMessage, false, providerInfo, cancellationToken).ConfigureAwait(false); + throw new HttpRequestException( + string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase), + null, + response.StatusCode); } private async Task<string> GetTokenInternal( @@ -702,13 +758,6 @@ namespace Jellyfin.LiveTv.Listings return false; } - // Clear tokens on any client error to avoid hammering SD with stale credentials - if (ex.StatusCode.HasValue && (int)ex.StatusCode.Value >= 400 && (int)ex.StatusCode.Value < 500) - { - _tokens.Clear(); - _lastErrorResponse = DateTime.UtcNow; - } - throw; } } |
