bug fixes (#774)
* add custom movie metadata parsing * refactor episode nfo reader * fix emby and jellyfin bugs
This commit is contained in:
@@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
### Fixed
|
### Fixed
|
||||||
- Fix adding episodes with no title to the search index
|
- Fix adding episodes with no title to the search index
|
||||||
- This behavior was preventing some items from being removed from the trash
|
- 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
|
## [0.5.3-beta] - 2022-04-29
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -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's Justice League</title>
|
||||||
|
<originaltitle>Zack Snyder'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'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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ namespace ErsatzTV.Core.Metadata;
|
|||||||
|
|
||||||
public class LocalMetadataProvider : ILocalMetadataProvider
|
public class LocalMetadataProvider : ILocalMetadataProvider
|
||||||
{
|
{
|
||||||
private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo));
|
|
||||||
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowNfo));
|
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowNfo));
|
||||||
private static readonly XmlSerializer ArtistSerializer = new(typeof(ArtistNfo));
|
private static readonly XmlSerializer ArtistSerializer = new(typeof(ArtistNfo));
|
||||||
private static readonly XmlSerializer MusicVideoSerializer = new(typeof(MusicVideoNfo));
|
private static readonly XmlSerializer MusicVideoSerializer = new(typeof(MusicVideoNfo));
|
||||||
@@ -25,6 +24,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
|||||||
private readonly ILogger<LocalMetadataProvider> _logger;
|
private readonly ILogger<LocalMetadataProvider> _logger;
|
||||||
|
|
||||||
private readonly IMetadataRepository _metadataRepository;
|
private readonly IMetadataRepository _metadataRepository;
|
||||||
|
private readonly IMovieNfoReader _movieNfoReader;
|
||||||
private readonly IMovieRepository _movieRepository;
|
private readonly IMovieRepository _movieRepository;
|
||||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||||
private readonly IOtherVideoRepository _otherVideoRepository;
|
private readonly IOtherVideoRepository _otherVideoRepository;
|
||||||
@@ -41,6 +41,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
|||||||
ISongRepository songRepository,
|
ISongRepository songRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
|
IMovieNfoReader movieNfoReader,
|
||||||
IEpisodeNfoReader episodeNfoReader,
|
IEpisodeNfoReader episodeNfoReader,
|
||||||
ILocalStatisticsProvider localStatisticsProvider,
|
ILocalStatisticsProvider localStatisticsProvider,
|
||||||
IClient client,
|
IClient client,
|
||||||
@@ -55,6 +56,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
|||||||
_songRepository = songRepository;
|
_songRepository = songRepository;
|
||||||
_fallbackMetadataProvider = fallbackMetadataProvider;
|
_fallbackMetadataProvider = fallbackMetadataProvider;
|
||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
|
_movieNfoReader = movieNfoReader;
|
||||||
_episodeNfoReader = episodeNfoReader;
|
_episodeNfoReader = episodeNfoReader;
|
||||||
_localStatisticsProvider = localStatisticsProvider;
|
_localStatisticsProvider = localStatisticsProvider;
|
||||||
_client = client;
|
_client = client;
|
||||||
@@ -936,21 +938,37 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
|
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
|
||||||
Option<MovieNfo> maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo;
|
Either<BaseError, MovieNfo> maybeNfo = await _movieNfoReader.Read(fileStream);
|
||||||
foreach (MovieNfo nfo in maybeNfo)
|
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 dateAdded = DateTime.UtcNow;
|
||||||
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
|
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
|
return new MovieMetadata
|
||||||
{
|
{
|
||||||
MetadataKind = MetadataKind.Sidecar,
|
MetadataKind = MetadataKind.Sidecar,
|
||||||
DateAdded = dateAdded,
|
DateAdded = dateAdded,
|
||||||
DateUpdated = dateUpdated,
|
DateUpdated = dateUpdated,
|
||||||
Title = nfo.Title,
|
Title = nfo.Title,
|
||||||
Year = nfo.Year,
|
SortTitle = nfo.SortTitle,
|
||||||
|
Year = year,
|
||||||
ContentRating = nfo.ContentRating,
|
ContentRating = nfo.ContentRating,
|
||||||
ReleaseDate = nfo.Premiered,
|
ReleaseDate = releaseDate,
|
||||||
Plot = nfo.Plot,
|
Plot = nfo.Plot,
|
||||||
Outline = nfo.Outline,
|
Outline = nfo.Outline,
|
||||||
Tagline = nfo.Tagline,
|
Tagline = nfo.Tagline,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
|
||||||
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
|
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.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)
|
public async Task<List<TvShowEpisodeNfo>> Read(Stream input)
|
||||||
{
|
{
|
||||||
@@ -31,38 +30,47 @@ public class EpisodeNfoReader : IEpisodeNfoReader
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "title":
|
case "title":
|
||||||
await ReadTitle(reader, nfo);
|
await ReadStringContent(reader, nfo, (episode, title) => episode.Title = title);
|
||||||
break;
|
break;
|
||||||
case "showtitle":
|
case "showtitle":
|
||||||
await ReadShowTitle(reader, nfo);
|
await ReadStringContent(reader, nfo, (episode, showTitle) => episode.ShowTitle = showTitle);
|
||||||
break;
|
break;
|
||||||
case "episode":
|
case "episode":
|
||||||
await ReadEpisode(reader, nfo);
|
await ReadIntContent(
|
||||||
|
reader,
|
||||||
|
nfo,
|
||||||
|
(episode, episodeNumber) => episode.Episode = episodeNumber);
|
||||||
break;
|
break;
|
||||||
case "season":
|
case "season":
|
||||||
await ReadSeason(reader, nfo);
|
await ReadIntContent(reader, nfo, (episode, seasonNumber) => episode.Season = seasonNumber);
|
||||||
break;
|
break;
|
||||||
case "uniqueid":
|
case "uniqueid":
|
||||||
await ReadUniqueId(reader, nfo);
|
await ReadUniqueId(reader, nfo, (episode, uniqueId) => episode.UniqueIds.Add(uniqueId));
|
||||||
break;
|
break;
|
||||||
case "mpaa":
|
case "mpaa":
|
||||||
await ReadContentRating(reader, nfo);
|
await ReadStringContent(
|
||||||
|
reader,
|
||||||
|
nfo,
|
||||||
|
(episode, contentRating) => episode.ContentRating = contentRating);
|
||||||
break;
|
break;
|
||||||
case "aired":
|
case "aired":
|
||||||
// TODO: parse the date here
|
// TODO: parse the date here
|
||||||
await ReadAired(reader, nfo);
|
await ReadAired(reader, nfo);
|
||||||
break;
|
break;
|
||||||
case "plot":
|
case "plot":
|
||||||
await ReadPlot(reader, nfo);
|
await ReadStringContent(reader, nfo, (episode, plot) => episode.Plot = plot);
|
||||||
break;
|
break;
|
||||||
case "actor":
|
case "actor":
|
||||||
ReadActor(reader, nfo);
|
ReadActor(reader, nfo, (episode, actor) => episode.Actors.Add(actor));
|
||||||
break;
|
break;
|
||||||
case "credits":
|
case "credits":
|
||||||
await ReadWriter(reader, nfo);
|
await ReadStringContent(reader, nfo, (episode, writer) => episode.Writers.Add(writer));
|
||||||
break;
|
break;
|
||||||
case "director":
|
case "director":
|
||||||
await ReadDirector(reader, nfo);
|
await ReadStringContent(
|
||||||
|
reader,
|
||||||
|
nfo,
|
||||||
|
(episode, director) => episode.Directors.Add(director));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,64 +94,6 @@ public class EpisodeNfoReader : IEpisodeNfoReader
|
|||||||
return result;
|
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)
|
private static async Task ReadAired(XmlReader reader, TvShowEpisodeNfo nfo)
|
||||||
{
|
{
|
||||||
if (nfo != null)
|
if (nfo != null)
|
||||||
@@ -151,47 +101,4 @@ public class EpisodeNfoReader : IEpisodeNfoReader
|
|||||||
nfo.Aired = await reader.ReadElementContentAsStringAsync();
|
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());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public class MovieNfo
|
|||||||
[XmlElement("title")]
|
[XmlElement("title")]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("sorttitle")]
|
||||||
|
public string SortTitle { get; set; }
|
||||||
|
|
||||||
[XmlElement("outline")]
|
[XmlElement("outline")]
|
||||||
public string Outline { get; set; }
|
public string Outline { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -292,7 +292,8 @@ public class EmbyApiClient : IEmbyApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
string path = item.Path ?? string.Empty;
|
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))
|
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -607,7 +608,8 @@ public class EmbyApiClient : IEmbyApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
string path = item.Path ?? string.Empty;
|
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))
|
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -351,7 +351,8 @@ public class JellyfinApiClient : IJellyfinApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
string path = item.Path ?? string.Empty;
|
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))
|
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -690,7 +691,8 @@ public class JellyfinApiClient : IJellyfinApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
string path = item.Path ?? string.Empty;
|
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))
|
if (path.StartsWith(pathInfo.NetworkPath, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ public class Startup
|
|||||||
services.AddScoped<IJellyfinSecretStore, JellyfinSecretStore>();
|
services.AddScoped<IJellyfinSecretStore, JellyfinSecretStore>();
|
||||||
services.AddScoped<IEmbySecretStore, EmbySecretStore>();
|
services.AddScoped<IEmbySecretStore, EmbySecretStore>();
|
||||||
services.AddScoped<IEpisodeNfoReader, EpisodeNfoReader>();
|
services.AddScoped<IEpisodeNfoReader, EpisodeNfoReader>();
|
||||||
|
services.AddScoped<IMovieNfoReader, MovieNfoReader>();
|
||||||
|
|
||||||
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
|
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user