diff --git a/CHANGELOG.md b/CHANGELOG.md index 4385d636e..1b966d32c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs b/ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs new file mode 100644 index 000000000..39e6f735a --- /dev/null +++ b/ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs @@ -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().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 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(@"")); + + Either 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( + @" +https://www.themoviedb.org/movie/11-star-wars")); + + Either 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( + @" + + Zack Snyder's Justice League + Zack Snyder's Justice League + Justice League 2 + + + 8.300000 + 197786 + + + 8.700000 + 3461 + + + 8.195670 + 4247 + + + 0 + 140 + + 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. + + 242 + https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdb873f474.jpg + https://image.tmdb.org/t/p/original/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg + https://assets.fanart.tv/fanart/movies/791373/moviethumb/zack-snyders-justice-league-6050310135cf6.jpg + https://image.tmdb.org/t/p/original/wcYBuOZDP6Vi8Ye4qax3Zx9dCan.jpg + https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdba9bdd16.jpg + https://assets.fanart.tv/fanart/movies/791373/hdmovielogo/zack-snyders-justice-league-5ed3f2e4952e9.png + https://assets.fanart.tv/fanart/movies/791373/moviebanner/zack-snyders-justice-league-6050049514d4c.jpg + + https://assets.fanart.tv/fanart/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg + https://image.tmdb.org/t/p/original/43NwryODVEsbBDC0jK3wYfVyb5q.jpg + + Australia:M + 0 + + 791373 + tt12361974 + 791373 + SuperHero + TV Recording + + Justice League Collection + Based on the DC Comics superhero team + + USA + Chris Terrio + Zack Snyder + 2021-03-18 + 2021 + + + + Warner Bros. Pictures + + + + + + + + eng + + + + + Ben Affleck + Bruce Wayne / Batman + 0 + https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg + + + Henry Cavill + Clark Kent / Superman / Kal-El + 1 + https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg + + + Gal Gadot + Diana Prince / Wonder Woman + 2 + https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg + + + 0.000000 + 0.000000 + + 2021-03-26 11:35:50 +")); + + Either 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 { "SuperHero" }); + nfo.Tags.Should().BeEquivalentTo(new List { "TV Recording" }); + nfo.Studios.Should().BeEquivalentTo(new List { "Warner Bros. Pictures" }); + nfo.Actors.Should().BeEquivalentTo( + new List + { + 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 { "Chris Terrio" }); + nfo.Directors.Should().BeEquivalentTo(new List { "Zack Snyder" }); + nfo.UniqueIds.Should().BeEquivalentTo( + new List + { + 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(@"Test Tag")); + + Either result = await _movieNfoReader.Read(stream); + + result.IsRight.Should().BeTrue(); + foreach (MovieNfo nfo in result.RightToSeq()) + { + nfo.Tags.Should().BeEquivalentTo(new List { "Test Tag" }); + } + } + + [Test] + public async Task MetadataNfo_With_Outline_Should_Return_Nfo() + { + await using var stream = + new MemoryStream(Encoding.UTF8.GetBytes(@"Test Outline")); + + Either result = await _movieNfoReader.Read(stream); + + result.IsRight.Should().BeTrue(); + foreach (MovieNfo nfo in result.RightToSeq()) + { + nfo.Outline.Should().Be("Test Outline"); + } + } +} diff --git a/ErsatzTV.Core/Errors/FailedToReadNfo.cs b/ErsatzTV.Core/Errors/FailedToReadNfo.cs new file mode 100644 index 000000000..f93081f21 --- /dev/null +++ b/ErsatzTV.Core/Errors/FailedToReadNfo.cs @@ -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}") + { + } +} diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs new file mode 100644 index 000000000..7f3264ad8 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core.Metadata.Nfo; + +namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; + +public interface IMovieNfoReader +{ + Task> Read(Stream input); +} diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 75411d0b6..ea54442a9 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -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 private readonly ILogger _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 ISongRepository songRepository, IFallbackMetadataProvider fallbackMetadataProvider, ILocalFileSystem localFileSystem, + IMovieNfoReader movieNfoReader, IEpisodeNfoReader episodeNfoReader, ILocalStatisticsProvider localStatisticsProvider, IClient client, @@ -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 try { await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); - Option maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo; - foreach (MovieNfo nfo in maybeNfo) + Either 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, diff --git a/ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs b/ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs index 42e1a0301..0f25f683c 100644 --- a/ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs +++ b/ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs @@ -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, IEpisodeNfoReader { public async Task> Read(Stream input) { @@ -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 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 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()); } diff --git a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs index f57e32366..2f868cc95 100644 --- a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs +++ b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs @@ -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; } diff --git a/ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs b/ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs new file mode 100644 index 000000000..504a68a38 --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs @@ -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, IMovieNfoReader +{ + private readonly IClient _client; + + public MovieNfoReader(IClient client) => _client = client; + + public async Task> 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(), + Tags = new List(), + Studios = new List(), + Actors = new List(), + Writers = new List(), + Directors = new List(), + UniqueIds = new List() + }; + 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()); + } + } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/NfoReader.cs b/ErsatzTV.Core/Metadata/Nfo/NfoReader.cs new file mode 100644 index 000000000..cffbcc77b --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/NfoReader.cs @@ -0,0 +1,83 @@ +using System.Xml; +using System.Xml.Linq; + +namespace ErsatzTV.Core.Metadata.Nfo; + +public abstract class NfoReader +{ + protected static async Task ReadStringContent(XmlReader reader, T nfo, Action action) + { + if (nfo != null) + { + string result = await reader.ReadElementContentAsStringAsync(); + action(nfo, result); + } + } + + protected static async Task ReadIntContent(XmlReader reader, T nfo, Action 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 action) + { + if (nfo != null && DateTime.TryParse(await reader.ReadElementContentAsStringAsync(), out DateTime result)) + { + action(nfo, result); + } + } + + protected static void ReadActor(XmlReader reader, T nfo, Action 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 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); + } + } +} diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index e97abb381..429fc6d32 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -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 } 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)) { diff --git a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs index d1fe8cd3f..83d566492 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs @@ -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 } 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)) { diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 8e66ebfd8..03503976b 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -411,6 +411,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));