Browse Source

fix issue reading xmltv fragments

pull/2771/head
Jason Dove 7 months ago
parent
commit
626f4cbad2
No known key found for this signature in database
  1. 1
      CHANGELOG.md
  2. 74
      ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs

1
CHANGELOG.md

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Restore default UI font that was erroneously removed in v26.1.1 - Restore default UI font that was erroneously removed in v26.1.1
- Use configured searching log level on startup, instead of the default log level of `Information` - Use configured searching log level on startup, instead of the default log level of `Information`
- MySql: fix searching for shows and seasons in schedule items editor - MySql: fix searching for shows and seasons in schedule items editor
- Fix 500 errors when serving XMLTV due to concurrent file reads and writes
## [26.1.1] - 2026-01-08 ## [26.1.1] - 2026-01-08
### Fixed ### Fixed

74
ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs

@ -11,30 +11,18 @@ using Microsoft.IO;
namespace ErsatzTV.Application.Channels; namespace ErsatzTV.Application.Channels;
public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<BaseError, ChannelGuide>> public partial class GetChannelGuideHandler(
IDbContextFactory<TvContext> dbContextFactory,
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem)
: IRequestHandler<GetChannelGuide, Either<BaseError, ChannelGuide>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly IFileSystem _fileSystem;
public GetChannelGuideHandler(
IDbContextFactory<TvContext> dbContextFactory,
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem)
{
_dbContextFactory = dbContextFactory;
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
}
public async Task<Either<BaseError, ChannelGuide>> Handle( public async Task<Either<BaseError, ChannelGuide>> Handle(
GetChannelGuide request, GetChannelGuide request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var hiddenChannelNumbers = dbContext.Channels var hiddenChannelNumbers = dbContext.Channels
.Where(c => c.ShowInEpg == false) .Where(c => c.ShowInEpg == false)
.Select(c => c.Number) .Select(c => c.Number)
@ -42,13 +30,13 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
.Select(n => $"{n}.xml") .Select(n => $"{n}.xml")
.ToImmutableHashSet(); .ToImmutableHashSet();
string channelsFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml"); string channelsFile = fileSystem.Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
if (!_fileSystem.File.Exists(channelsFile)) if (!fileSystem.File.Exists(channelsFile))
{ {
return BaseError.New($"Required file {channelsFile} is missing"); return BaseError.New($"Required file {channelsFile} is missing");
} }
long mtime = File.GetLastWriteTime(channelsFile).Ticks; long mtime = fileSystem.File.GetLastWriteTime(channelsFile).Ticks;
var accessTokenUri = $"?v={mtime}"; var accessTokenUri = $"?v={mtime}";
if (!string.IsNullOrWhiteSpace(request.AccessToken)) if (!string.IsNullOrWhiteSpace(request.AccessToken))
@ -56,7 +44,7 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
accessTokenUri += $"&amp;access_token={request.AccessToken}"; accessTokenUri += $"&amp;access_token={request.AccessToken}";
} }
string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken); string channelsFragment = await SharedReadAllText(channelsFile, cancellationToken);
// TODO: is regex faster? // TODO: is regex faster?
channelsFragment = channelsFragment channelsFragment = channelsFragment
@ -65,30 +53,52 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
var channelDataFragments = new Dictionary<string, string>(); var channelDataFragments = new Dictionary<string, string>();
foreach (string fileName in _localFileSystem.ListFiles(FileSystemLayout.ChannelGuideCacheFolder)) foreach (string fileName in localFileSystem.ListFiles(FileSystemLayout.ChannelGuideCacheFolder))
{ {
if (fileName.Contains("channels")) if (fileName.Contains("channels"))
{ {
continue; continue;
} }
if (hiddenChannelNumbers.Contains(Path.GetFileName(fileName))) if (hiddenChannelNumbers.Contains(fileSystem.Path.GetFileName(fileName)))
{ {
continue; continue;
} }
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken); try
{
string channelDataFragment = await SharedReadAllText(fileName, cancellationToken);
channelDataFragment = channelDataFragment channelDataFragment = channelDataFragment
.Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}") .Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}")
.Replace("{AccessTokenUri}", accessTokenUri); .Replace("{AccessTokenUri}", accessTokenUri);
channelDataFragment = EtvTagRegex().Replace(channelDataFragment, string.Empty); channelDataFragment = EtvTagRegex().Replace(channelDataFragment, string.Empty);
channelDataFragments.Add(Path.GetFileNameWithoutExtension(fileName), channelDataFragment); channelDataFragments.Add(fileSystem.Path.GetFileNameWithoutExtension(fileName), channelDataFragment);
}
catch (FileNotFoundException)
{
// ignore this channel fragment
}
catch (IOException)
{
// ignore this channel fragment
}
} }
return new ChannelGuide(_recyclableMemoryStreamManager, channelsFragment, channelDataFragments); return new ChannelGuide(recyclableMemoryStreamManager, channelsFragment, channelDataFragments);
}
private async Task<string> SharedReadAllText(string fileName, CancellationToken cancellationToken)
{
await using var stream = fileSystem.FileStream.New(
fileName,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite);
using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadToEndAsync(cancellationToken);
} }
[GeneratedRegex(@"<etv:[^>]+?>.*?<\/etv:[^>]+?>|<etv:[^>]+?\/>", RegexOptions.Singleline)] [GeneratedRegex(@"<etv:[^>]+?>.*?<\/etv:[^>]+?>|<etv:[^>]+?\/>", RegexOptions.Singleline)]

Loading…
Cancel
Save