Browse Source

nfo and memory fixes (#789)

* partial episode nfo metadata

* nfo metadata reliability fixes

* use recyclable memory streams
pull/790/head
Jason Dove 4 years ago committed by GitHub
parent
commit
44dd68fe59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs
  3. 9
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  4. 58
      ErsatzTV.Core.Tests/Metadata/Nfo/ArtistNfoReaderTests.cs
  5. 40
      ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs
  6. 7
      ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs
  7. 7
      ErsatzTV.Core.Tests/Metadata/Nfo/MusicVideoNfoReaderTests.cs
  8. 7
      ErsatzTV.Core.Tests/Metadata/Nfo/OtherVideoNfoReaderTests.cs
  9. 7
      ErsatzTV.Core.Tests/Metadata/Nfo/TvShowNfoReaderTests.cs
  10. 6
      ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo
  11. 7
      ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo
  12. 6
      ErsatzTV.Core.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo
  13. 1
      ErsatzTV.Core/ErsatzTV.Core.csproj
  14. 2
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs
  15. 2
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs
  16. 2
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs
  17. 2
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs
  18. 2
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs
  19. 2
      ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs
  20. 11
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  21. 18
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  22. 34
      ErsatzTV.Core/Metadata/Nfo/ArtistNfoReader.cs
  23. 53
      ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs
  24. 34
      ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs
  25. 34
      ErsatzTV.Core/Metadata/Nfo/MusicVideoNfoReader.cs
  26. 173
      ErsatzTV.Core/Metadata/Nfo/NfoReader.cs
  27. 34
      ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs
  28. 34
      ErsatzTV.Core/Metadata/Nfo/TvShowNfoReader.cs
  29. 26
      ErsatzTV.Infrastructure/Plex/PlexEtag.cs
  30. 11
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  31. 1
      ErsatzTV.sln.DotSettings
  32. 4
      ErsatzTV/Startup.cs

7
CHANGELOG.md

@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Reduce memory use due to library scan operations
### Changed
- Replace invalid (control) characters in NFO metadata with replacement character `<EFBFBD>` before parsing
- Store partial (incomplete) NFO metadata results when invalid XML is encountered
- Previously, no metadata would be stored if the XML within the NFO failed to validate
## [0.5.6-beta] - 2022-05-06
### Fixed

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

@ -1,15 +1,21 @@ @@ -1,15 +1,21 @@
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv;
using Microsoft.IO;
namespace ErsatzTV.Application.Channels;
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
{
private readonly IChannelRepository _channelRepository;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetChannelGuideHandler(IChannelRepository channelRepository, RecyclableMemoryStreamManager recyclableMemoryStreamManager)
{
_channelRepository = channelRepository;
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
}
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
_channelRepository.GetAllForGuide()
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
.Map(channels => new ChannelGuide(_recyclableMemoryStreamManager, request.Scheme, request.Host, channels));
}

9
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -37,12 +37,21 @@ @@ -37,12 +37,21 @@
<Content Include="Resources\ErsatzTV.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\ArtistInvalidCharacters1.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\ArtistInvalidCharacters2.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\test.sup">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\test.srt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\EpisodeInvalidCharacters.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

58
ErsatzTV.Core.Tests/Metadata/Nfo/ArtistNfoReaderTests.cs

@ -2,8 +2,11 @@ @@ -2,8 +2,11 @@
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
using Serilog;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
@ -11,7 +14,24 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo; @@ -11,7 +14,24 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo;
public class ArtistNfoReaderTests
{
[SetUp]
public void SetUp() => _artistNfoReader = new ArtistNfoReader(new Mock<IClient>().Object);
public void SetUp() => _artistNfoReader = new ArtistNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
_logger);
private readonly ILogger<ArtistNfoReader> _logger;
public ArtistNfoReaderTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
_logger = loggerFactory.CreateLogger<ArtistNfoReader>();
}
private ArtistNfoReader _artistNfoReader;
@ -153,6 +173,42 @@ Joel attended Hicksville High School in 1967, but he did not graduate with his c @@ -153,6 +173,42 @@ Joel attended Hicksville High School in 1967, but he did not graduate with his c
}
}
[Test]
public async Task Invalid_Characters_End_Should_Abort_And_Return_Nfo()
{
string sourceFile = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"Resources",
"Nfo",
"ArtistInvalidCharacters1.nfo");
Either<BaseError, ArtistNfo> result = await _artistNfoReader.ReadFromFile(sourceFile);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Name.Should().Be("Test Name");
}
}
[Test]
public async Task Invalid_Characters_Middle_Should_Continue_And_Return_Nfo()
{
string sourceFile = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"Resources",
"Nfo",
"ArtistInvalidCharacters2.nfo");
Either<BaseError, ArtistNfo> result = await _artistNfoReader.ReadFromFile(sourceFile);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Name.Should().Be("Test Name");
nfo.Moods.Should().BeEquivalentTo(new List<string> { "Test Mood" });
nfo.Styles.Count.Should().Be(1);
}
}
private static string NormalizeLineEndingsLF(string str) =>
str
.Replace("\r\n", "\n")

40
ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs

@ -2,8 +2,11 @@ @@ -2,8 +2,11 @@
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
using Serilog;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
@ -11,7 +14,24 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo; @@ -11,7 +14,24 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo;
public class EpisodeNfoReaderTests
{
[SetUp]
public void SetUp() => _episodeNfoReader = new EpisodeNfoReader(new Mock<IClient>().Object);
public void SetUp() => _episodeNfoReader = new EpisodeNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
_logger);
private readonly ILogger<EpisodeNfoReader> _logger;
public EpisodeNfoReaderTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
_logger = loggerFactory.CreateLogger<EpisodeNfoReader>();
}
private EpisodeNfoReader _episodeNfoReader;
@ -404,4 +424,22 @@ public class EpisodeNfoReaderTests @@ -404,4 +424,22 @@ public class EpisodeNfoReaderTests
});
}
}
[Test]
public async Task Invalid_Characters_Should_Abort_And_Return_Nfo()
{
string sourceFile = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"Resources",
"Nfo",
"EpisodeInvalidCharacters.nfo");
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.ReadFromFile(sourceFile);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Title.Should().Be("Test Title");
}
}
}

7
ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo; @@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo;
public class MovieNfoReaderTests
{
[SetUp]
public void SetUp() => _movieNfoReader = new MovieNfoReader(new Mock<IClient>().Object);
public void SetUp() => _movieNfoReader = new MovieNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<MovieNfoReader>());
private MovieNfoReader _movieNfoReader;

7
ErsatzTV.Core.Tests/Metadata/Nfo/MusicVideoNfoReaderTests.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo; @@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo;
public class MusicVideoNfoReaderTests
{
[SetUp]
public void SetUp() => _musicVideoNfoReader = new MusicVideoNfoReader(new Mock<IClient>().Object);
public void SetUp() => _musicVideoNfoReader = new MusicVideoNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<MusicVideoNfoReader>());
private MusicVideoNfoReader _musicVideoNfoReader;

7
ErsatzTV.Core.Tests/Metadata/Nfo/OtherVideoNfoReaderTests.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo; @@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo;
public class OtherVideoNfoReaderTests
{
[SetUp]
public void SetUp() => _otherVideoNfoReader = new OtherVideoNfoReader(new Mock<IClient>().Object);
public void SetUp() => _otherVideoNfoReader = new OtherVideoNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<OtherVideoNfoReader>());
private OtherVideoNfoReader _otherVideoNfoReader;

7
ErsatzTV.Core.Tests/Metadata/Nfo/TvShowNfoReaderTests.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo; @@ -11,7 +13,10 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo;
public class TvShowNfoReaderTests
{
[SetUp]
public void SetUp() => _tvShowNfoReader = new TvShowNfoReader(new Mock<IClient>().Object);
public void SetUp() => _tvShowNfoReader = new TvShowNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<TvShowNfoReader>());
private TvShowNfoReader _tvShowNfoReader;

6
ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--created on whatever - comment-->
<artist>
<name>Test Name</name>
</artist>
ÐPS½NÞ5Þ*˜¡¡ã·Ýq×ÍâeVk—¯¬}É

7
ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--created on whatever - comment-->
<artist>
<name>Test Name</name>
<style>ÐPS½NÞ5Þ*˜¡¡ã·Ýq×ÍâeVk—¯¬}É</style>
<mood>Test Mood</mood>
</artist>

6
ErsatzTV.Core.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--created on whatever - comment-->
<episodedetails>
<title>Test Title</title>
</episodedetails>
ÐPS½NÞ5Þ*˜¡¡ã·Ýq×ÍâeVk—¯¬}É

1
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.1.46">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

2
ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs

@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; @@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IArtistNfoReader
{
Task<Either<BaseError, ArtistNfo>> Read(Stream input);
Task<Either<BaseError, ArtistNfo>> ReadFromFile(string fileName);
}

2
ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs

@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; @@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IEpisodeNfoReader
{
Task<Either<BaseError, List<TvShowEpisodeNfo>>> Read(Stream input);
Task<Either<BaseError, List<TvShowEpisodeNfo>>> ReadFromFile(string fileName);
}

2
ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs

@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; @@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IMovieNfoReader
{
Task<Either<BaseError, MovieNfo>> Read(Stream input);
Task<Either<BaseError, MovieNfo>> ReadFromFile(string fileName);
}

2
ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs

@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; @@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IMusicVideoNfoReader
{
Task<Either<BaseError, MusicVideoNfo>> Read(Stream input);
Task<Either<BaseError, MusicVideoNfo>> ReadFromFile(string fileName);
}

2
ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs

@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; @@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IOtherVideoNfoReader
{
Task<Either<BaseError, OtherVideoNfo>> Read(Stream input);
Task<Either<BaseError, OtherVideoNfo>> ReadFromFile(string fileName);
}

2
ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs

@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; @@ -4,5 +4,5 @@ namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface ITvShowNfoReader
{
Task<Either<BaseError, TvShowNfo>> Read(Stream input);
Task<Either<BaseError, TvShowNfo>> ReadFromFile(string fileName);
}

11
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -4,6 +4,7 @@ using ErsatzTV.Core.Domain; @@ -4,6 +4,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin;
using Microsoft.IO;
using Serilog;
namespace ErsatzTV.Core.Iptv;
@ -12,10 +13,16 @@ public class ChannelGuide @@ -12,10 +13,16 @@ public class ChannelGuide
{
private readonly List<Channel> _channels;
private readonly string _host;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly string _scheme;
public ChannelGuide(string scheme, string host, List<Channel> channels)
public ChannelGuide(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
string scheme,
string host,
List<Channel> channels)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_scheme = scheme;
_host = host;
_channels = channels;
@ -23,7 +30,7 @@ public class ChannelGuide @@ -23,7 +30,7 @@ public class ChannelGuide
public string ToXml()
{
using var ms = new MemoryStream();
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
using var xml = XmlWriter.Create(ms);
xml.WriteStartDocument();

18
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -231,8 +231,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -231,8 +231,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Either<BaseError, MusicVideoNfo> maybeNfo = await _musicVideoNfoReader.Read(fileStream);
Either<BaseError, MusicVideoNfo> maybeNfo = await _musicVideoNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
@ -976,8 +975,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -976,8 +975,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Either<BaseError, TvShowNfo> maybeNfo = await _tvShowNfoReader.Read(fileStream);
Either<BaseError, TvShowNfo> maybeNfo = await _tvShowNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
@ -1027,8 +1025,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1027,8 +1025,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Either<BaseError, ArtistNfo> maybeNfo = await _artistNfoReader.Read(fileStream);
Either<BaseError, ArtistNfo> maybeNfo = await _artistNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
@ -1067,8 +1064,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1067,8 +1064,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Either<BaseError, List<TvShowEpisodeNfo>> maybeNfo = await _episodeNfoReader.Read(fileStream);
Either<BaseError, List<TvShowEpisodeNfo>> maybeNfo = await _episodeNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
@ -1123,8 +1119,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1123,8 +1119,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Either<BaseError, MovieNfo> maybeNfo = await _movieNfoReader.Read(fileStream);
Either<BaseError, MovieNfo> maybeNfo = await _movieNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
@ -1199,8 +1194,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1199,8 +1194,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Either<BaseError, OtherVideoNfo> maybeNfo = await _otherVideoNfoReader.Read(fileStream);
Either<BaseError, OtherVideoNfo> maybeNfo = await _otherVideoNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(

34
ErsatzTV.Core/Metadata/Nfo/ArtistNfoReader.cs

@ -2,22 +2,43 @@ @@ -2,22 +2,43 @@
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Core.Metadata.Nfo;
public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader
{
private readonly IClient _client;
private readonly ILogger<ArtistNfoReader> _logger;
public ArtistNfoReader(IClient client) => _client = client;
public ArtistNfoReader(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IClient client,
ILogger<ArtistNfoReader> logger)
: base(recyclableMemoryStreamManager, logger)
{
_client = client;
_logger = logger;
}
public async Task<Either<BaseError, ArtistNfo>> Read(Stream input)
public async Task<Either<BaseError, ArtistNfo>> ReadFromFile(string fileName)
{
// ReSharper disable once ConvertToUsingDeclaration
await using (Stream s = await SanitizedStreamForFile(fileName))
{
return await Read(s);
}
}
internal async Task<Either<BaseError, ArtistNfo>> Read(Stream input)
{
ArtistNfo nfo = null;
try
{
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
ArtistNfo nfo = null;
var done = false;
while (!done && await reader.ReadAsync())
@ -74,6 +95,13 @@ public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader @@ -74,6 +95,13 @@ public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (XmlException ex) when (ex.Message.Contains(
"invalid character",
StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogWarning("Invalid XML detected; returning incomplete metadata");
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);

53
ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs

@ -2,23 +2,42 @@ @@ -2,23 +2,42 @@
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Core.Metadata.Nfo;
public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
{
private readonly IClient _client;
private readonly ILogger<EpisodeNfoReader> _logger;
public EpisodeNfoReader(IClient client) => _client = client;
public EpisodeNfoReader(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IClient client,
ILogger<EpisodeNfoReader> logger)
: base(recyclableMemoryStreamManager, logger)
{
_client = client;
_logger = logger;
}
public async Task<Either<BaseError, List<TvShowEpisodeNfo>>> Read(Stream input)
public async Task<Either<BaseError, List<TvShowEpisodeNfo>>> ReadFromFile(string fileName)
{
try
// ReSharper disable once ConvertToUsingDeclaration
await using (Stream s = await SanitizedStreamForFile(fileName))
{
var result = new List<TvShowEpisodeNfo>();
return await Read(s);
}
}
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
internal async Task<Either<BaseError, List<TvShowEpisodeNfo>>> Read(Stream input)
{
var result = new List<TvShowEpisodeNfo>();
try
{
using var reader = XmlReader.Create(input, Settings);
TvShowEpisodeNfo nfo = null;
while (await reader.ReadAsync())
@ -36,6 +55,8 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader @@ -36,6 +55,8 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
Writers = new List<string>(),
Directors = new List<string>()
};
// immediately add so we have something to return if we encounter invalid characters
result.Add(nfo);
break;
case "title":
await ReadStringContent(reader, nfo, (episode, title) => episode.Title = title);
@ -87,25 +108,19 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader @@ -87,25 +108,19 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
break;
}
break;
case XmlNodeType.EndElement:
switch (reader.Name.ToLowerInvariant())
{
case "episodedetails":
if (nfo != null)
{
result.Add(nfo);
}
break;
}
break;
}
}
return result;
}
catch (XmlException ex) when (ex.Message.Contains(
"invalid character",
StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogWarning("Invalid XML detected; returning incomplete metadata");
return result;
}
catch (Exception ex)
{
_client.Notify(ex);

34
ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs

@ -2,22 +2,43 @@ @@ -2,22 +2,43 @@
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Core.Metadata.Nfo;
public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader
{
private readonly IClient _client;
private readonly ILogger<MovieNfoReader> _logger;
public MovieNfoReader(IClient client) => _client = client;
public MovieNfoReader(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IClient client,
ILogger<MovieNfoReader> logger)
: base(recyclableMemoryStreamManager, logger)
{
_client = client;
_logger = logger;
}
public async Task<Either<BaseError, MovieNfo>> Read(Stream input)
public async Task<Either<BaseError, MovieNfo>> ReadFromFile(string fileName)
{
// ReSharper disable once ConvertToUsingDeclaration
await using (Stream s = await SanitizedStreamForFile(fileName))
{
return await Read(s);
}
}
internal async Task<Either<BaseError, MovieNfo>> Read(Stream input)
{
MovieNfo nfo = null;
try
{
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
MovieNfo nfo = null;
var done = false;
while (!done && await reader.ReadAsync())
@ -105,6 +126,13 @@ public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader @@ -105,6 +126,13 @@ public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (XmlException ex) when (ex.Message.Contains(
"invalid character",
StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogWarning("Invalid XML detected; returning incomplete metadata");
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);

34
ErsatzTV.Core/Metadata/Nfo/MusicVideoNfoReader.cs

@ -2,22 +2,43 @@ @@ -2,22 +2,43 @@
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Core.Metadata.Nfo;
public class MusicVideoNfoReader : NfoReader<MusicVideoNfo>, IMusicVideoNfoReader
{
private readonly IClient _client;
private readonly ILogger<MusicVideoNfoReader> _logger;
public MusicVideoNfoReader(IClient client) => _client = client;
public MusicVideoNfoReader(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IClient client,
ILogger<MusicVideoNfoReader> logger)
: base(recyclableMemoryStreamManager, logger)
{
_client = client;
_logger = logger;
}
public async Task<Either<BaseError, MusicVideoNfo>> Read(Stream input)
public async Task<Either<BaseError, MusicVideoNfo>> ReadFromFile(string fileName)
{
// ReSharper disable once ConvertToUsingDeclaration
await using (Stream s = await SanitizedStreamForFile(fileName))
{
return await Read(s);
}
}
internal async Task<Either<BaseError, MusicVideoNfo>> Read(Stream input)
{
MusicVideoNfo nfo = null;
try
{
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
MusicVideoNfo nfo = null;
var done = false;
while (!done && await reader.ReadAsync())
@ -87,6 +108,13 @@ public class MusicVideoNfoReader : NfoReader<MusicVideoNfo>, IMusicVideoNfoReade @@ -87,6 +108,13 @@ public class MusicVideoNfoReader : NfoReader<MusicVideoNfo>, IMusicVideoNfoReade
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (XmlException ex) when (ex.Message.Contains(
"invalid character",
StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogWarning("Invalid XML detected; returning incomplete metadata");
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);

173
ErsatzTV.Core/Metadata/Nfo/NfoReader.cs

@ -1,83 +1,166 @@ @@ -1,83 +1,166 @@
using System.Xml;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Core.Metadata.Nfo;
public abstract class NfoReader<T>
{
protected static async Task ReadStringContent(XmlReader reader, T nfo, Action<T, string> action)
{
if (nfo != null)
private static readonly byte[] Buffer = new byte[8 * 1024 * 1024];
private static readonly Regex Pattern = new(@"[\p{C}-[\r\n\t]]+");
protected static readonly XmlReaderSettings Settings =
new()
{
string result = await reader.ReadElementContentAsStringAsync();
action(nfo, result);
}
Async = true,
ConformanceLevel = ConformanceLevel.Fragment,
ValidationType = ValidationType.None,
CheckCharacters = false,
IgnoreProcessingInstructions = true,
IgnoreComments = true
};
private readonly ILogger _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
protected NfoReader(RecyclableMemoryStreamManager recyclableMemoryStreamManager, ILogger logger)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_logger = logger;
}
protected static async Task ReadIntContent(XmlReader reader, T nfo, Action<T, int> action)
protected async Task<Stream> SanitizedStreamForFile(string fileName)
{
if (nfo != null && int.TryParse(await reader.ReadElementContentAsStringAsync(), out int result))
using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, Buffer.Length, true))
{
action(nfo, result);
while (await fs.ReadAsync(Buffer) > 0)
{
// read the file
}
string text = Encoding.UTF8.GetString(Buffer);
// trim BOM and zero width space, replace controls with replacement character
string stripped = Pattern.Replace(text.Trim('\uFEFF', '\u200B'), "\ufffd");
MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
await ms.WriteAsync(Encoding.UTF8.GetBytes(stripped));
ms.Position = 0;
return ms;
}
}
protected static async Task ReadDateTimeContent(XmlReader reader, T nfo, Action<T, DateTime> action)
protected async Task ReadStringContent(XmlReader reader, T nfo, Action<T, string> action)
{
if (nfo != null && DateTime.TryParse(await reader.ReadElementContentAsStringAsync(), out DateTime result))
try
{
action(nfo, result);
if (nfo != null)
{
string result = await reader.ReadElementContentAsStringAsync();
action(nfo, result);
}
}
catch (XmlException ex)
{
_logger.LogWarning(ex, "Error reading string content from NFO {ElementName}", reader.Name);
}
}
protected static void ReadActor(XmlReader reader, T nfo, Action<T, ActorNfo> action)
protected async Task ReadIntContent(XmlReader reader, T nfo, Action<T, int> action)
{
if (nfo != null)
try
{
var actor = new ActorNfo();
var element = (XElement)XNode.ReadFrom(reader);
XElement name = element.Element("name");
if (name != null)
if (nfo != null && int.TryParse(await reader.ReadElementContentAsStringAsync(), out int result))
{
actor.Name = name.Value;
action(nfo, result);
}
}
catch (XmlException ex)
{
_logger.LogWarning(ex, "Error reading int content from NFO {ElementName}", reader.Name);
}
}
XElement role = element.Element("role");
if (role != null)
protected async Task ReadDateTimeContent(XmlReader reader, T nfo, Action<T, DateTime> action)
{
try
{
if (nfo != null && DateTime.TryParse(await reader.ReadElementContentAsStringAsync(), out DateTime result))
{
actor.Role = role.Value;
action(nfo, result);
}
}
catch (XmlException ex)
{
_logger.LogWarning(ex, "Error reading date content from NFO {ElementName}", reader.Name);
}
}
XElement order = element.Element("order");
if (order != null && int.TryParse(order.Value, out int orderValue))
protected void ReadActor(XmlReader reader, T nfo, Action<T, ActorNfo> action)
{
try
{
if (nfo != null)
{
actor.Order = orderValue;
}
var actor = new ActorNfo();
var element = (XElement)XNode.ReadFrom(reader);
XElement thumb = element.Element("thumb");
if (thumb != null)
{
actor.Thumb = thumb.Value;
}
XElement name = element.Element("name");
if (name != null)
{
actor.Name = name.Value;
}
XElement role = element.Element("role");
if (role != null)
{
actor.Role = role.Value;
}
action(nfo, actor);
XElement order = element.Element("order");
if (order != null && int.TryParse(order.Value, out int orderValue))
{
actor.Order = orderValue;
}
XElement thumb = element.Element("thumb");
if (thumb != null)
{
actor.Thumb = thumb.Value;
}
action(nfo, actor);
}
}
catch (XmlException ex)
{
_logger.LogWarning(ex, "Error reading actor content from NFO {ElementName}", reader.Name);
}
}
protected static async Task ReadUniqueId(XmlReader reader, T nfo, Action<T, UniqueIdNfo> action)
protected async Task ReadUniqueId(XmlReader reader, T nfo, Action<T, UniqueIdNfo> action)
{
if (nfo != null)
try
{
if (nfo != null)
{
var uniqueId = new UniqueIdNfo();
reader.MoveToAttribute("default");
uniqueId.Default = bool.TryParse(reader.Value, out bool def) && def;
reader.MoveToAttribute("type");
uniqueId.Type = reader.Value;
reader.MoveToElement();
uniqueId.Guid = await reader.ReadElementContentAsStringAsync();
action(nfo, uniqueId);
}
}
catch (XmlException ex)
{
var uniqueId = new UniqueIdNfo();
reader.MoveToAttribute("default");
uniqueId.Default = bool.TryParse(reader.Value, out bool def) && def;
reader.MoveToAttribute("type");
uniqueId.Type = reader.Value;
reader.MoveToElement();
uniqueId.Guid = await reader.ReadElementContentAsStringAsync();
action(nfo, uniqueId);
_logger.LogWarning(ex, "Error reading uniqueid content from NFO {ElementName}", reader.Name);
}
}
}

34
ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs

@ -2,22 +2,43 @@ @@ -2,22 +2,43 @@
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Core.Metadata.Nfo;
public class OtherVideoNfoReader : NfoReader<OtherVideoNfo>, IOtherVideoNfoReader
{
private readonly IClient _client;
private readonly ILogger<OtherVideoNfoReader> _logger;
public OtherVideoNfoReader(IClient client) => _client = client;
public OtherVideoNfoReader(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IClient client,
ILogger<OtherVideoNfoReader> logger)
: base(recyclableMemoryStreamManager, logger)
{
_client = client;
_logger = logger;
}
public async Task<Either<BaseError, OtherVideoNfo>> Read(Stream input)
public async Task<Either<BaseError, OtherVideoNfo>> ReadFromFile(string fileName)
{
// ReSharper disable once ConvertToUsingDeclaration
await using (Stream s = await SanitizedStreamForFile(fileName))
{
return await Read(s);
}
}
internal async Task<Either<BaseError, OtherVideoNfo>> Read(Stream input)
{
OtherVideoNfo nfo = null;
try
{
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
OtherVideoNfo nfo = null;
var done = false;
while (!done && await reader.ReadAsync())
@ -105,6 +126,13 @@ public class OtherVideoNfoReader : NfoReader<OtherVideoNfo>, IOtherVideoNfoReade @@ -105,6 +126,13 @@ public class OtherVideoNfoReader : NfoReader<OtherVideoNfo>, IOtherVideoNfoReade
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (XmlException ex) when (ex.Message.Contains(
"invalid character",
StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogWarning("Invalid XML detected; returning incomplete metadata");
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);

34
ErsatzTV.Core/Metadata/Nfo/TvShowNfoReader.cs

@ -2,22 +2,43 @@ @@ -2,22 +2,43 @@
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Core.Metadata.Nfo;
public class TvShowNfoReader : NfoReader<TvShowNfo>, ITvShowNfoReader
{
private readonly IClient _client;
private readonly ILogger<TvShowNfoReader> _logger;
public TvShowNfoReader(IClient client) => _client = client;
public TvShowNfoReader(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IClient client,
ILogger<TvShowNfoReader> logger)
: base(recyclableMemoryStreamManager, logger)
{
_client = client;
_logger = logger;
}
public async Task<Either<BaseError, TvShowNfo>> Read(Stream input)
public async Task<Either<BaseError, TvShowNfo>> ReadFromFile(string fileName)
{
// ReSharper disable once ConvertToUsingDeclaration
await using (Stream s = await SanitizedStreamForFile(fileName))
{
return await Read(s);
}
}
internal async Task<Either<BaseError, TvShowNfo>> Read(Stream input)
{
TvShowNfo nfo = null;
try
{
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
TvShowNfo nfo = null;
var done = false;
while (!done && await reader.ReadAsync())
@ -91,6 +112,13 @@ public class TvShowNfoReader : NfoReader<TvShowNfo>, ITvShowNfoReader @@ -91,6 +112,13 @@ public class TvShowNfoReader : NfoReader<TvShowNfo>, ITvShowNfoReader
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (XmlException ex) when (ex.Message.Contains(
"invalid character",
StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogWarning("Invalid XML detected; returning incomplete metadata");
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);

26
ErsatzTV.Infrastructure/Plex/PlexEtag.cs

@ -1,13 +1,21 @@ @@ -1,13 +1,21 @@
using System.Security.Cryptography;
using ErsatzTV.Infrastructure.Plex.Models;
using Microsoft.IO;
namespace ErsatzTV.Infrastructure.Plex;
public static class PlexEtag
public class PlexEtag
{
public static string ForMovie(PlexMetadataResponse response)
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public PlexEtag(RecyclableMemoryStreamManager recyclableMemoryStreamManager)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
}
public string ForMovie(PlexMetadataResponse response)
{
using var ms = new MemoryStream();
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
using var bw = new BinaryWriter(ms);
// video key
@ -80,9 +88,9 @@ public static class PlexEtag @@ -80,9 +88,9 @@ public static class PlexEtag
return BitConverter.ToString(hash).Replace("-", string.Empty);
}
public static string ForShow(PlexMetadataResponse response)
public string ForShow(PlexMetadataResponse response)
{
using var ms = new MemoryStream();
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
using var bw = new BinaryWriter(ms);
// video key
@ -127,9 +135,9 @@ public static class PlexEtag @@ -127,9 +135,9 @@ public static class PlexEtag
return BitConverter.ToString(hash).Replace("-", string.Empty);
}
public static string ForSeason(PlexXmlMetadataResponse response)
public string ForSeason(PlexXmlMetadataResponse response)
{
using var ms = new MemoryStream();
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
using var bw = new BinaryWriter(ms);
// video key
@ -165,9 +173,9 @@ public static class PlexEtag @@ -165,9 +173,9 @@ public static class PlexEtag
return BitConverter.ToString(hash).Replace("-", string.Empty);
}
public static string ForEpisode(PlexXmlMetadataResponse response)
public string ForEpisode(PlexXmlMetadataResponse response)
{
using var ms = new MemoryStream();
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
using var bw = new BinaryWriter(ms);
// video key

11
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -13,13 +13,16 @@ namespace ErsatzTV.Infrastructure.Plex; @@ -13,13 +13,16 @@ namespace ErsatzTV.Infrastructure.Plex;
public class PlexServerApiClient : IPlexServerApiClient
{
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly PlexEtag _plexEtag;
private readonly ILogger<PlexServerApiClient> _logger;
public PlexServerApiClient(
IFallbackMetadataProvider fallbackMetadataProvider,
PlexEtag plexEtag,
ILogger<PlexServerApiClient> logger)
{
_fallbackMetadataProvider = fallbackMetadataProvider;
_plexEtag = plexEtag;
_logger = logger;
}
@ -344,7 +347,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -344,7 +347,7 @@ public class PlexServerApiClient : IPlexServerApiClient
var movie = new PlexMovie
{
Etag = PlexEtag.ForMovie(response),
Etag = _plexEtag.ForMovie(response),
Key = response.Key,
MovieMetadata = new List<MovieMetadata> { metadata },
MediaVersions = new List<MediaVersion> { version },
@ -530,7 +533,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -530,7 +533,7 @@ public class PlexServerApiClient : IPlexServerApiClient
var show = new PlexShow
{
Key = response.Key,
Etag = PlexEtag.ForShow(response),
Etag = _plexEtag.ForShow(response),
ShowMetadata = new List<ShowMetadata> { metadata },
TraktListItems = new List<TraktListItem>()
};
@ -701,7 +704,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -701,7 +704,7 @@ public class PlexServerApiClient : IPlexServerApiClient
var season = new PlexSeason
{
Key = response.Key,
Etag = PlexEtag.ForSeason(response),
Etag = _plexEtag.ForSeason(response),
SeasonNumber = response.Index,
SeasonMetadata = new List<SeasonMetadata> { metadata },
TraktListItems = new List<TraktListItem>()
@ -742,7 +745,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -742,7 +745,7 @@ public class PlexServerApiClient : IPlexServerApiClient
var episode = new PlexEpisode
{
Key = response.Key,
Etag = PlexEtag.ForEpisode(response),
Etag = _plexEtag.ForEpisode(response),
EpisodeMetadata = new List<EpisodeMetadata> { metadata },
MediaVersions = new List<MediaVersion> { version },
TraktListItems = new List<TraktListItem>()

1
ErsatzTV.sln.DotSettings

@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NV/@EntryIndexedValue">NV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SAR/@EntryIndexedValue">SAR</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SDH/@EntryIndexedValue">SDH</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UTF/@EntryIndexedValue">UTF</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=YUV/@EntryIndexedValue">YUV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=YUVJ/@EntryIndexedValue">YUVJ</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=anull/@EntryIndexedValue">True</s:Boolean>

4
ErsatzTV/Startup.cs

@ -56,6 +56,7 @@ using MediatR.Courier.DependencyInjection; @@ -56,6 +56,7 @@ using MediatR.Courier.DependencyInjection;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using Microsoft.IO;
using MudBlazor.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@ -322,6 +323,7 @@ public class Startup @@ -322,6 +323,7 @@ public class Startup
services.AddSingleton<IFFmpegSegmenterService, FFmpegSegmenterService>();
services.AddSingleton<ITempFilePool, TempFilePool>();
services.AddSingleton<IHlsPlaylistFilter, HlsPlaylistFilter>();
services.AddSingleton<RecyclableMemoryStreamManager>();
AddChannel<IBackgroundServiceRequest>(services);
AddChannel<IPlexBackgroundServiceRequest>(services);
AddChannel<IJellyfinBackgroundServiceRequest>(services);
@ -417,6 +419,8 @@ public class Startup @@ -417,6 +419,8 @@ public class Startup
services.AddScoped<ITvShowNfoReader, TvShowNfoReader>();
services.AddScoped<IOtherVideoNfoReader, OtherVideoNfoReader>();
services.AddScoped<PlexEtag>();
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
// run-once/blocking startup services

Loading…
Cancel
Save