aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-02-11 09:44:37 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-02-20 14:35:56 +0100
commite49d71707c5f9f46fca373922a1ac1893cfc6ad5 (patch)
treed45643fa57ddc7b4fab4a53f09fc2492fbee05c0 /src
parent9da046abc168190e78bcb93a3496adbf3e79bf01 (diff)
Fix EPG issues
Diffstat (limited to 'src')
-rw-r--r--src/Jellyfin.LiveTv/DefaultLiveTvService.cs5
-rw-r--r--src/Jellyfin.LiveTv/Listings/ListingsManager.cs34
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs55
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs49
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs28
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