Browse Source

bug fixes (#774)

* add custom movie metadata parsing

* refactor episode nfo reader

* fix emby and jellyfin bugs
pull/775/head
Jason Dove 4 years ago committed by GitHub
parent
commit
1ebc4b62e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 241
      ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs
  3. 9
      ErsatzTV.Core/Errors/FailedToReadNfo.cs
  4. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs
  5. 28
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  6. 133
      ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs
  7. 3
      ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs
  8. 114
      ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs
  9. 83
      ErsatzTV.Core/Metadata/Nfo/NfoReader.cs
  10. 6
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  11. 6
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  12. 1
      ErsatzTV/Startup.cs

6
CHANGELOG.md

@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Fix adding episodes with no title to the search index
- This behavior was preventing some items from being removed from the trash
- Support combination NFO metadata for movies
- Note that ErsatzTV does not scrape any metadata; any URLs after the XML will be ignored
- Fix bug causing some Jellyfin and Emby content to incorrectly show as unavailable
### Added
- Use `Sort Title` from Movie NFO metadata if available
## [0.5.3-beta] - 2022-04-29
### Fixed

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

@ -0,0 +1,241 @@ @@ -0,0 +1,241 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class MovieNfoReaderTests
{
[SetUp]
public void SetUp() => _movieNfoReader = new MovieNfoReader(new Mock<IClient>().Object);
private MovieNfoReader _movieNfoReader;
[Test]
public async Task ParsingNfo_Should_Return_Error()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie></movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<movie></movie>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task FullSample_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
<movie>
<title>Zack Snyder&apos;s Justice League</title>
<originaltitle>Zack Snyder&apos;s Justice League</originaltitle>
<sorttitle>Justice League 2</sorttitle>
<ratings>
<rating name=""imdb"" max=""10"" default=""true"">
<value>8.300000</value>
<votes>197786</votes>
</rating>
<rating name=""themoviedb"" max=""10"">
<value>8.700000</value>
<votes>3461</votes>
</rating>
<rating name=""trakt"" max=""10"">
<value>8.195670</value>
<votes>4247</votes>
</rating>
</ratings>
<userrating>0</userrating>
<top250>140</top250>
<outline></outline>
<plot>Determined to ensure Superman&apos;s ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.</plot>
<tagline></tagline>
<runtime>242</runtime>
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdb873f474.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://image.tmdb.org/t/p/original/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviethumb/zack-snyders-justice-league-6050310135cf6.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://image.tmdb.org/t/p/original/wcYBuOZDP6Vi8Ye4qax3Zx9dCan.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""keyart"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdba9bdd16.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview="""">https://assets.fanart.tv/fanart/movies/791373/hdmovielogo/zack-snyders-justice-league-5ed3f2e4952e9.png</thumb>
<thumb spoof="""" cache="""" aspect=""banner"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviebanner/zack-snyders-justice-league-6050049514d4c.jpg</thumb>
<fanart>
<thumb colors="""" preview=""https://assets.fanart.tv/preview/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg"">https://assets.fanart.tv/fanart/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg</thumb>
<thumb colors="""" preview=""https://image.tmdb.org/t/p/w780/43NwryODVEsbBDC0jK3wYfVyb5q.jpg"">https://image.tmdb.org/t/p/original/43NwryODVEsbBDC0jK3wYfVyb5q.jpg</thumb>
</fanart>
<mpaa>Australia:M</mpaa>
<playcount>0</playcount>
<lastplayed></lastplayed>
<id>791373</id>
<uniqueid type=""imdb"">tt12361974</uniqueid>
<uniqueid type=""tmdb"" default=""true"">791373</uniqueid>
<genre>SuperHero</genre>
<tag>TV Recording</tag>
<set>
<name>Justice League Collection</name>
<overview>Based on the DC Comics superhero team</overview>
</set>
<country>USA</country>
<credits>Chris Terrio</credits>
<director>Zack Snyder</director>
<premiered>2021-03-18</premiered>
<year>2021</year>
<status></status>
<code></code>
<aired></aired>
<studio>Warner Bros. Pictures</studio>
<trailer></trailer>
<fileinfo>
<streamdetails>
<video>
<codec>hevc</codec>
<aspect>1.777778</aspect>
<width>1920</width>
<height>1080</height>
<durationinseconds>14528</durationinseconds>
<stereomode></stereomode>
</video>
<audio>
<codec>ac3</codec>
<language>eng</language>
<channels>6</channels>
</audio>
<audio>
<codec>ac3</codec>
<language>fre</language>
<channels>6</channels>
</audio>
<subtitle>
<language>eng</language>
</subtitle>
</streamdetails>
</fileinfo>
<actor>
<name>Ben Affleck</name>
<role>Bruce Wayne / Batman</role>
<order>0</order>
<thumb>https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg</thumb>
</actor>
<actor>
<name>Henry Cavill</name>
<role>Clark Kent / Superman / Kal-El</role>
<order>1</order>
<thumb>https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg</thumb>
</actor>
<actor>
<name>Gal Gadot</name>
<role>Diana Prince / Wonder Woman</role>
<order>2</order>
<thumb>https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg</thumb>
</actor>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2021-03-26 11:35:50</dateadded>
</movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MovieNfo nfo in result.RightToSeq())
{
nfo.Title.Should().Be("Zack Snyder's Justice League");
nfo.SortTitle.Should().Be("Justice League 2");
nfo.Outline.Should().BeNullOrEmpty();
nfo.Year.Should().Be(2021);
nfo.ContentRating.Should().Be("Australia:M");
nfo.Premiered.Should().Be(new DateTime(2021, 03, 18));
nfo.Plot.Should().Be(
"Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.");
nfo.Tagline.Should().BeNullOrEmpty();
nfo.Genres.Should().BeEquivalentTo(new List<string> { "SuperHero" });
nfo.Tags.Should().BeEquivalentTo(new List<string> { "TV Recording" });
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Warner Bros. Pictures" });
nfo.Actors.Should().BeEquivalentTo(
new List<ActorNfo>
{
new()
{
Name = "Ben Affleck", Order = 0, Role = "Bruce Wayne / Batman",
Thumb = "https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg"
},
new()
{
Name = "Henry Cavill", Order = 1, Role = "Clark Kent / Superman / Kal-El",
Thumb = "https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg"
},
new()
{
Name = "Gal Gadot", Order = 2, Role = "Diana Prince / Wonder Woman",
Thumb = "https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg"
}
});
nfo.Writers.Should().BeEquivalentTo(new List<string> { "Chris Terrio" });
nfo.Directors.Should().BeEquivalentTo(new List<string> { "Zack Snyder" });
nfo.UniqueIds.Should().BeEquivalentTo(
new List<UniqueIdNfo>
{
new() { Type = "imdb", Guid = "tt12361974", Default = false },
new() { Type = "tmdb", Guid = "791373", Default = true }
});
}
}
[Test]
public async Task MetadataNfo_With_Tag_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><tag>Test Tag</tag></movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MovieNfo nfo in result.RightToSeq())
{
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
}
}
[Test]
public async Task MetadataNfo_With_Outline_Should_Return_Nfo()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><outline>Test Outline</outline></movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MovieNfo nfo in result.RightToSeq())
{
nfo.Outline.Should().Be("Test Outline");
}
}
}

9
ErsatzTV.Core/Errors/FailedToReadNfo.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Core.Errors;
public class FailedToReadNfo : BaseError
{
public FailedToReadNfo(string message = null) : base(
string.IsNullOrWhiteSpace(message) ? "Failed to read NFO metadata" : $"Failed to read NFO metadata: {message}")
{
}
}

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

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core.Metadata.Nfo;
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IMovieNfoReader
{
Task<Either<BaseError, MovieNfo>> Read(Stream input);
}

28
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -12,7 +12,6 @@ namespace ErsatzTV.Core.Metadata; @@ -12,7 +12,6 @@ namespace ErsatzTV.Core.Metadata;
public class LocalMetadataProvider : ILocalMetadataProvider
{
private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo));
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowNfo));
private static readonly XmlSerializer ArtistSerializer = new(typeof(ArtistNfo));
private static readonly XmlSerializer MusicVideoSerializer = new(typeof(MusicVideoNfo));
@ -25,6 +24,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -25,6 +24,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
private readonly ILogger<LocalMetadataProvider> _logger;
private readonly IMetadataRepository _metadataRepository;
private readonly IMovieNfoReader _movieNfoReader;
private readonly IMovieRepository _movieRepository;
private readonly IMusicVideoRepository _musicVideoRepository;
private readonly IOtherVideoRepository _otherVideoRepository;
@ -41,6 +41,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -41,6 +41,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
ISongRepository songRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILocalFileSystem localFileSystem,
IMovieNfoReader movieNfoReader,
IEpisodeNfoReader episodeNfoReader,
ILocalStatisticsProvider localStatisticsProvider,
IClient client,
@ -55,6 +56,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -55,6 +56,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
_songRepository = songRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_localFileSystem = localFileSystem;
_movieNfoReader = movieNfoReader;
_episodeNfoReader = episodeNfoReader;
_localStatisticsProvider = localStatisticsProvider;
_client = client;
@ -936,21 +938,37 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -936,21 +938,37 @@ public class LocalMetadataProvider : ILocalMetadataProvider
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<MovieNfo> maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo;
foreach (MovieNfo nfo in maybeNfo)
Either<BaseError, MovieNfo> maybeNfo = await _movieNfoReader.Read(fileStream);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
"Failed to read Movie nfo metadata from {Path}: {Error}",
nfoFileName,
error.ToString());
return _fallbackMetadataProvider.GetFallbackMetadata(movie);
}
foreach (MovieNfo nfo in maybeNfo.RightToSeq())
{
DateTime dateAdded = DateTime.UtcNow;
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
int year = nfo.Year > 0 ? nfo.Year : nfo.Premiered.Year;
DateTime releaseDate = nfo.Premiered > SystemTime.MinValueUtc
? nfo.Premiered
: new DateTimeOffset(year, 0, 0, 0, 0, 0, TimeSpan.Zero).UtcDateTime;
return new MovieMetadata
{
MetadataKind = MetadataKind.Sidecar,
DateAdded = dateAdded,
DateUpdated = dateUpdated,
Title = nfo.Title,
Year = nfo.Year,
SortTitle = nfo.SortTitle,
Year = year,
ContentRating = nfo.ContentRating,
ReleaseDate = nfo.Premiered,
ReleaseDate = releaseDate,
Plot = nfo.Plot,
Outline = nfo.Outline,
Tagline = nfo.Tagline,

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

@ -1,10 +1,9 @@ @@ -1,10 +1,9 @@
using System.Xml;
using System.Xml.Linq;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
namespace ErsatzTV.Core.Metadata.Nfo;
public class EpisodeNfoReader : IEpisodeNfoReader
public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
{
public async Task<List<TvShowEpisodeNfo>> Read(Stream input)
{
@ -31,38 +30,47 @@ public class EpisodeNfoReader : IEpisodeNfoReader @@ -31,38 +30,47 @@ public class EpisodeNfoReader : IEpisodeNfoReader
};
break;
case "title":
await ReadTitle(reader, nfo);
await ReadStringContent(reader, nfo, (episode, title) => episode.Title = title);
break;
case "showtitle":
await ReadShowTitle(reader, nfo);
await ReadStringContent(reader, nfo, (episode, showTitle) => episode.ShowTitle = showTitle);
break;
case "episode":
await ReadEpisode(reader, nfo);
await ReadIntContent(
reader,
nfo,
(episode, episodeNumber) => episode.Episode = episodeNumber);
break;
case "season":
await ReadSeason(reader, nfo);
await ReadIntContent(reader, nfo, (episode, seasonNumber) => episode.Season = seasonNumber);
break;
case "uniqueid":
await ReadUniqueId(reader, nfo);
await ReadUniqueId(reader, nfo, (episode, uniqueId) => episode.UniqueIds.Add(uniqueId));
break;
case "mpaa":
await ReadContentRating(reader, nfo);
await ReadStringContent(
reader,
nfo,
(episode, contentRating) => episode.ContentRating = contentRating);
break;
case "aired":
// TODO: parse the date here
await ReadAired(reader, nfo);
break;
case "plot":
await ReadPlot(reader, nfo);
await ReadStringContent(reader, nfo, (episode, plot) => episode.Plot = plot);
break;
case "actor":
ReadActor(reader, nfo);
ReadActor(reader, nfo, (episode, actor) => episode.Actors.Add(actor));
break;
case "credits":
await ReadWriter(reader, nfo);
await ReadStringContent(reader, nfo, (episode, writer) => episode.Writers.Add(writer));
break;
case "director":
await ReadDirector(reader, nfo);
await ReadStringContent(
reader,
nfo,
(episode, director) => episode.Directors.Add(director));
break;
}
@ -86,64 +94,6 @@ public class EpisodeNfoReader : IEpisodeNfoReader @@ -86,64 +94,6 @@ public class EpisodeNfoReader : IEpisodeNfoReader
return result;
}
private static async Task ReadTitle(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
{
nfo.Title = await reader.ReadElementContentAsStringAsync();
}
}
private static async Task ReadShowTitle(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
{
nfo.ShowTitle = await reader.ReadElementContentAsStringAsync();
}
}
private static async Task ReadEpisode(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
{
bool _ = int.TryParse(await reader.ReadElementContentAsStringAsync(), out int episode);
nfo.Episode = episode;
}
}
private static async Task ReadSeason(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
{
bool _ = int.TryParse(await reader.ReadElementContentAsStringAsync(), out int season);
nfo.Season = season;
}
}
private static async Task ReadUniqueId(XmlReader reader, TvShowEpisodeNfo nfo)
{
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();
nfo.UniqueIds.Add(uniqueId);
}
}
private static async Task ReadContentRating(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
{
nfo.ContentRating = await reader.ReadElementContentAsStringAsync();
}
}
private static async Task ReadAired(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
@ -151,47 +101,4 @@ public class EpisodeNfoReader : IEpisodeNfoReader @@ -151,47 +101,4 @@ public class EpisodeNfoReader : IEpisodeNfoReader
nfo.Aired = await reader.ReadElementContentAsStringAsync();
}
}
private static async Task ReadPlot(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
{
nfo.Plot = await reader.ReadElementContentAsStringAsync();
}
}
private static void ReadActor(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
{
var actor = new ActorNfo();
var element = (XElement)XNode.ReadFrom(reader);
XElement name = element.Element("name");
if (name != null)
{
actor.Name = name.Value;
}
XElement role = element.Element("role");
if (role != null)
{
actor.Role = role.Value;
}
XElement thumb = element.Element("thumb");
if (thumb != null)
{
actor.Thumb = thumb.Value;
}
nfo.Actors.Add(actor);
}
}
private static async Task ReadWriter(XmlReader reader, TvShowEpisodeNfo nfo) =>
nfo?.Writers.Add(await reader.ReadElementContentAsStringAsync());
private static async Task ReadDirector(XmlReader reader, TvShowEpisodeNfo nfo) =>
nfo?.Directors.Add(await reader.ReadElementContentAsStringAsync());
}

3
ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs

@ -8,6 +8,9 @@ public class MovieNfo @@ -8,6 +8,9 @@ public class MovieNfo
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("sorttitle")]
public string SortTitle { get; set; }
[XmlElement("outline")]
public string Outline { get; set; }

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

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
using System.Xml;
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
namespace ErsatzTV.Core.Metadata.Nfo;
public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader
{
private readonly IClient _client;
public MovieNfoReader(IClient client) => _client = client;
public async Task<Either<BaseError, MovieNfo>> Read(Stream input)
{
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())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
switch (reader.Name.ToLowerInvariant())
{
case "movie":
nfo = new MovieNfo
{
Genres = new List<string>(),
Tags = new List<string>(),
Studios = new List<string>(),
Actors = new List<ActorNfo>(),
Writers = new List<string>(),
Directors = new List<string>(),
UniqueIds = new List<UniqueIdNfo>()
};
break;
case "title":
await ReadStringContent(reader, nfo, (movie, title) => movie.Title = title);
break;
case "sorttitle":
await ReadStringContent(reader, nfo, (movie, sortTitle) => movie.SortTitle = sortTitle);
break;
case "outline":
await ReadStringContent(reader, nfo, (movie, outline) => movie.Outline = outline);
break;
case "year":
await ReadIntContent(reader, nfo, (movie, year) => movie.Year = year);
break;
case "mpaa":
await ReadStringContent(
reader,
nfo,
(movie, contentRating) => movie.ContentRating = contentRating);
break;
case "premiered":
await ReadDateTimeContent(
reader,
nfo,
(movie, premiered) => movie.Premiered = premiered);
break;
case "plot":
await ReadStringContent(reader, nfo, (movie, plot) => movie.Plot = plot);
break;
case "genre":
await ReadStringContent(reader, nfo, (movie, genre) => movie.Genres.Add(genre));
break;
case "tag":
await ReadStringContent(reader, nfo, (movie, tag) => movie.Tags.Add(tag));
break;
case "studio":
await ReadStringContent(reader, nfo, (movie, studio) => movie.Studios.Add(studio));
break;
case "actor":
ReadActor(reader, nfo, (movie, actor) => movie.Actors.Add(actor));
break;
case "credits":
await ReadStringContent(reader, nfo, (movie, writer) => movie.Writers.Add(writer));
break;
case "director":
await ReadStringContent(
reader,
nfo,
(movie, director) => movie.Directors.Add(director));
break;
case "uniqueid":
await ReadUniqueId(reader, nfo, (movie, uniqueid) => movie.UniqueIds.Add(uniqueid));
break;
}
break;
case XmlNodeType.EndElement:
if (reader.Name == "movie")
{
done = true;
}
break;
}
}
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);
return new FailedToReadNfo(ex.ToString());
}
}
}

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

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
using System.Xml;
using System.Xml.Linq;
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)
{
string result = await reader.ReadElementContentAsStringAsync();
action(nfo, result);
}
}
protected static async Task ReadIntContent(XmlReader reader, T nfo, Action<T, int> action)
{
if (nfo != null && int.TryParse(await reader.ReadElementContentAsStringAsync(), out int result))
{
action(nfo, result);
}
}
protected static async Task ReadDateTimeContent(XmlReader reader, T nfo, Action<T, DateTime> action)
{
if (nfo != null && DateTime.TryParse(await reader.ReadElementContentAsStringAsync(), out DateTime result))
{
action(nfo, result);
}
}
protected static void ReadActor(XmlReader reader, T nfo, Action<T, ActorNfo> action)
{
if (nfo != null)
{
var actor = new ActorNfo();
var element = (XElement)XNode.ReadFrom(reader);
XElement name = element.Element("name");
if (name != null)
{
actor.Name = name.Value;
}
XElement role = element.Element("role");
if (role != null)
{
actor.Role = role.Value;
}
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);
}
}
protected static async Task ReadUniqueId(XmlReader reader, T nfo, Action<T, UniqueIdNfo> action)
{
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);
}
}
}

6
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -292,7 +292,8 @@ public class EmbyApiClient : IEmbyApiClient @@ -292,7 +292,8 @@ public class EmbyApiClient : IEmbyApiClient
}
string path = item.Path ?? string.Empty;
foreach (EmbyPathInfo pathInfo in library.PathInfos)
foreach (EmbyPathInfo pathInfo in
library.PathInfos.Filter(pi => !string.IsNullOrWhiteSpace(pi.NetworkPath)))
{
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
{
@ -607,7 +608,8 @@ public class EmbyApiClient : IEmbyApiClient @@ -607,7 +608,8 @@ public class EmbyApiClient : IEmbyApiClient
}
string path = item.Path ?? string.Empty;
foreach (EmbyPathInfo pathInfo in library.PathInfos)
foreach (EmbyPathInfo pathInfo in
library.PathInfos.Filter(pi => !string.IsNullOrWhiteSpace(pi.NetworkPath)))
{
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
{

6
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -351,7 +351,8 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -351,7 +351,8 @@ public class JellyfinApiClient : IJellyfinApiClient
}
string path = item.Path ?? string.Empty;
foreach (JellyfinPathInfo pathInfo in library.PathInfos)
foreach (JellyfinPathInfo pathInfo in library.PathInfos.Filter(
pi => !string.IsNullOrWhiteSpace(pi.NetworkPath)))
{
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
{
@ -690,7 +691,8 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -690,7 +691,8 @@ public class JellyfinApiClient : IJellyfinApiClient
}
string path = item.Path ?? string.Empty;
foreach (JellyfinPathInfo pathInfo in library.PathInfos)
foreach (JellyfinPathInfo pathInfo in library.PathInfos.Filter(
pi => !string.IsNullOrWhiteSpace(pi.NetworkPath)))
{
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
{

1
ErsatzTV/Startup.cs

@ -411,6 +411,7 @@ public class Startup @@ -411,6 +411,7 @@ public class Startup
services.AddScoped<IJellyfinSecretStore, JellyfinSecretStore>();
services.AddScoped<IEmbySecretStore, EmbySecretStore>();
services.AddScoped<IEpisodeNfoReader, EpisodeNfoReader>();
services.AddScoped<IMovieNfoReader, MovieNfoReader>();
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));

Loading…
Cancel
Save