diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-02-11 09:44:37 +0100 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-02-20 14:35:56 +0100 |
| commit | e49d71707c5f9f46fca373922a1ac1893cfc6ad5 (patch) | |
| tree | d45643fa57ddc7b4fab4a53f09fc2492fbee05c0 /src | |
| parent | 9da046abc168190e78bcb93a3496adbf3e79bf01 (diff) | |
Fix EPG issues
Diffstat (limited to 'src')
| -rw-r--r-- | src/Jellyfin.LiveTv/DefaultLiveTvService.cs | 5 | ||||
| -rw-r--r-- | src/Jellyfin.LiveTv/Listings/ListingsManager.cs | 34 | ||||
| -rw-r--r-- | src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs | 55 | ||||
| -rw-r--r-- | src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs | 49 | ||||
| -rw-r--r-- | src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs | 28 |
5 files changed, 134 insertions, 37 deletions
diff --git a/src/Jellyfin.LiveTv/DefaultLiveTvService.cs b/src/Jellyfin.LiveTv/DefaultLiveTvService.cs index d8f873abe6..d477bc3713 100644 --- a/src/Jellyfin.LiveTv/DefaultLiveTvService.cs +++ b/src/Jellyfin.LiveTv/DefaultLiveTvService.cs @@ -774,7 +774,10 @@ namespace Jellyfin.LiveTv } } - SearchForDuplicateShowIds(enabledTimersForSeries); + if (seriesTimer.SkipEpisodesInLibrary) + { + SearchForDuplicateShowIds(enabledTimersForSeries); + } if (deleteInvalidTimers) { diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs index 39c2bd375b..a37204cc57 100644 --- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs +++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -74,6 +75,9 @@ public class ListingsManager : IListingsManager } _config.SaveConfiguration("livetv", config); + + InvalidateListingsProviderCache(info.Id); + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); return info; @@ -87,6 +91,12 @@ public class ListingsManager : IListingsManager config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); _config.SaveConfiguration("livetv", config); + + if (!string.IsNullOrEmpty(id)) + { + InvalidateListingsProviderCache(id); + } + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); } @@ -322,6 +332,30 @@ public class ListingsManager : IListingsManager return channelId; } + private void InvalidateListingsProviderCache(string providerId) + { + // Clear in-memory EPG channel cache for this provider + _epgChannels.TryRemove(providerId, out _); + + // Delete the cached XMLTV file so a fresh copy is downloaded + var cachePath = _config.CommonApplicationPaths?.CachePath; + if (!string.IsNullOrEmpty(cachePath)) + { + var xmltvCacheFile = Path.Combine(cachePath, "xmltv", providerId + ".xml"); + if (File.Exists(xmltvCacheFile)) + { + try + { + File.Delete(xmltvCacheFile); + } + catch (IOException ex) + { + _logger.LogWarning(ex, "Error deleting XMLTV cache file for provider {ProviderId}", providerId); + } + } + } + } + private async Task<EpgChannelData> GetEpgChannels( IListingsProvider provider, ListingsProviderInfo info, diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs index d6f15906ef..939fd0f66d 100644 --- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs @@ -149,7 +149,7 @@ namespace Jellyfin.LiveTv.Listings var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays); if (willBeCached && images is not null) { - var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]); + var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId); if (imageIndex > -1) { var programEntry = programDict[schedule.ProgramId]; @@ -458,32 +458,32 @@ namespace Jellyfin.LiveTv.Listings return []; } - StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); - foreach (var i in programIds) + // SD API accepts max 500 program IDs per request + const int BatchSize = 500; + var results = new List<ShowImagesDto>(); + for (int i = 0; i < programIds.Count; i += BatchSize) { - str.Append('"') - .Append(i[..10]) - .Append("\","); - } - - // Remove last , - str.Length--; - str.Append(']'); + var batch = programIds.Skip(i).Take(BatchSize); - using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs"); - message.Headers.TryAddWithoutValidation("token", token); - message.Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json); + using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs"); + message.Headers.TryAddWithoutValidation("token", token); + message.Content = JsonContent.Create(batch, options: _jsonOptions); - try - { - return await Request<IReadOnlyList<ShowImagesDto>>(message, true, info, cancellationToken).ConfigureAwait(false); + try + { + var batchResult = await Request<IReadOnlyList<ShowImagesDto>>(message, true, info, cancellationToken).ConfigureAwait(false); + if (batchResult is not null) + { + results.AddRange(batchResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting image info from schedules direct"); + } } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting image info from schedules direct"); - return []; - } + return results; } public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, string country, string location, CancellationToken cancellationToken) @@ -547,7 +547,7 @@ namespace Jellyfin.LiveTv.Listings } // Avoid hammering SD - if ((DateTime.UtcNow - _lastErrorResponse).TotalMinutes < 1) + if ((DateTime.UtcNow - _lastErrorResponse).TotalMinutes < 30) { return null; } @@ -579,7 +579,7 @@ namespace Jellyfin.LiveTv.Listings } catch (HttpRequestException ex) { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest) + if (ex.StatusCode.HasValue && (int)ex.StatusCode.Value >= 400 && (int)ex.StatusCode.Value < 500) { _tokens.Clear(); _lastErrorResponse = DateTime.UtcNow; @@ -702,6 +702,13 @@ 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; } } diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index 7938b7a6e4..0b73c6776f 100644 --- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -79,25 +79,39 @@ namespace Jellyfin.LiveTv.Listings Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); } - if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + try { - _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path); + if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false); - var redirectedUrl = response.RequestMessage?.RequestUri?.ToString() ?? info.Path; - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false); + var redirectedUrl = response.RequestMessage?.RequestUri?.ToString() ?? info.Path; + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).ConfigureAwait(false); + } + } + else { - return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).ConfigureAwait(false); + var stream = AsyncFile.OpenRead(info.Path); + await using (stream.ConfigureAwait(false)) + { + return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); + } } } - else + catch (Exception ex) { - var stream = AsyncFile.OpenRead(info.Path); - await using (stream.ConfigureAwait(false)) + _logger.LogError(ex, "Error downloading or processing XMLTV file from {Path}", info.Path); + + if (File.Exists(cacheFile)) { - return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); + File.Delete(cacheFile); } + + throw; } } @@ -130,9 +144,20 @@ namespace Jellyfin.LiveTv.Listings { await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } + } + + var fileInfo = new FileInfo(file); + if (!fileInfo.Exists || fileInfo.Length == 0) + { + if (fileInfo.Exists) + { + File.Delete(file); + } - return file; + throw new InvalidOperationException("Downloaded XMLTV file is empty: " + originalUrl); } + + return file; } public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) diff --git a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs index d67f77bc0a..4043d7399e 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading; @@ -100,6 +101,33 @@ public class TunerHostManager : ITunerHostManager } /// <inheritdoc /> + public void DeleteTunerHost(string? id) + { + var config = _config.GetLiveTvConfiguration(); + config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); + _config.SaveConfiguration("livetv", config); + + // Clean up the disk cache file for this tuner + if (!string.IsNullOrEmpty(id)) + { + var channelCacheFile = Path.Combine(_config.CommonApplicationPaths.CachePath, id + "_channels"); + if (File.Exists(channelCacheFile)) + { + try + { + File.Delete(channelCacheFile); + } + catch (IOException ex) + { + _logger.LogWarning(ex, "Error deleting channel cache file for tuner {TunerId}", id); + } + } + } + + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); + } + + /// <inheritdoc /> public async IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly) { var configuredDeviceIds = _config.GetLiveTvConfiguration().TunerHosts |
