mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-12 19:56:30 +01:00
Fix EPG issues
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -774,7 +774,10 @@ namespace Jellyfin.LiveTv
|
||||
}
|
||||
}
|
||||
|
||||
SearchForDuplicateShowIds(enabledTimersForSeries);
|
||||
if (seriesTimer.SkipEpisodesInLibrary)
|
||||
{
|
||||
SearchForDuplicateShowIds(enabledTimersForSeries);
|
||||
}
|
||||
|
||||
if (deleteInvalidTimers)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user