Fix EPG issues

This commit is contained in:
Shadowghost
2026-02-11 09:44:37 +01:00
parent 9da046abc1
commit e49d71707c
8 changed files with 146 additions and 45 deletions

View File

@@ -992,9 +992,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteTunerHost([FromQuery] string? id)
{
var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_configurationManager.SaveConfiguration("livetv", config);
_tunerHostManager.DeleteTunerHost(id);
return NoContent();
}

View File

@@ -37,6 +37,12 @@ public interface ITunerHostManager
/// <returns>The <see cref="TunerHostInfo"/>s.</returns>
IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly);
/// <summary>
/// Deletes a tuner host by id, cleans up associated caches, and triggers a guide refresh.
/// </summary>
/// <param name="id">The tuner host id to delete.</param>
void DeleteTunerHost(string? id);
/// <summary>
/// Scans for tuner devices that have changed URLs.
/// </summary>

View File

@@ -109,7 +109,7 @@ namespace MediaBrowser.Controller.LiveTv
{
if (double.TryParse(Number, CultureInfo.InvariantCulture, out double number))
{
return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty);
return string.Format(CultureInfo.InvariantCulture, "{0:0000000000.00000}", number) + "-" + (Name ?? string.Empty);
}
return (Number ?? string.Empty) + "-" + (Name ?? string.Empty);

View File

@@ -774,7 +774,10 @@ namespace Jellyfin.LiveTv
}
}
SearchForDuplicateShowIds(enabledTimersForSeries);
if (seriesTimer.SkipEpisodesInLibrary)
{
SearchForDuplicateShowIds(enabledTimersForSeries);
}
if (deleteInvalidTimers)
{

View File

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

View File

@@ -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("\",");
var batch = programIds.Skip(i).Take(BatchSize);
using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs");
message.Headers.TryAddWithoutValidation("token", token);
message.Content = JsonContent.Create(batch, options: _jsonOptions);
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");
}
}
// Remove last ,
str.Length--;
str.Append(']');
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);
try
{
return await Request<IReadOnlyList<ShowImagesDto>>(message, true, info, cancellationToken).ConfigureAwait(false);
}
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;
}
}

View File

@@ -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);
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))
if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).ConfigureAwait(false);
_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))
{
return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).ConfigureAwait(false);
}
}
else
{
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);
}
return file;
}
var fileInfo = new FileInfo(file);
if (!fileInfo.Exists || fileInfo.Length == 0)
{
if (fileInfo.Exists)
{
File.Delete(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)

View File

@@ -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;
@@ -99,6 +100,33 @@ public class TunerHostManager : ITunerHostManager
return info;
}
/// <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)
{