Browse Source

more local metadata parsing improvements (#776)

* extract remaining nfo xml serializers

* add artist nfo tests

* add music video nfo tests

* add tv show nfo reader tests

* custom artist nfo reader

* custom music video nfo reader

* custom tv show nfo reader

* local metadata parsing cleanup

* update changelog
pull/778/head
Jason Dove 4 years ago committed by GitHub
parent
commit
ecb6ed37f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 153
      ErsatzTV.Core.Tests/Metadata/Nfo/ArtistNfoReaderTests.cs
  3. 283
      ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs
  4. 8
      ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs
  5. 156
      ErsatzTV.Core.Tests/Metadata/Nfo/MusicVideoNfoReaderTests.cs
  6. 216
      ErsatzTV.Core.Tests/Metadata/Nfo/TvShowNfoReaderTests.cs
  7. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs
  8. 2
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs
  9. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs
  10. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs
  11. 116
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  12. 83
      ErsatzTV.Core/Metadata/Nfo/ArtistNfoReader.cs
  13. 179
      ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs
  14. 2
      ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs
  15. 3
      ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs
  16. 92
      ErsatzTV.Core/Metadata/Nfo/MusicVideoNfoReader.cs
  17. 2
      ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs
  18. 2
      ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs
  19. 100
      ErsatzTV.Core/Metadata/Nfo/TvShowNfoReader.cs
  20. 3
      ErsatzTV/Startup.cs

2
CHANGELOG.md

@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -7,7 +7,7 @@ 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
- Support combination NFO metadata for movies, shows, artists and music videos
- 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

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

@ -0,0 +1,153 @@ @@ -0,0 +1,153 @@
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 ArtistNfoReaderTests
{
[SetUp]
public void SetUp() => _artistNfoReader = new ArtistNfoReader(new Mock<IClient>().Object);
private ArtistNfoReader _artistNfoReader;
[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, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<artist></artist>"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<artist></artist>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.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"" ?>
<artist>
<name>Billy Joel</name>
<musicBrainzArtistID>64b94289-9474-4d43-8c93-918ccc1920d1</musicBrainzArtistID>
<sortname>Joel, Billy</sortname>
<type>Person</type>
<gender>Male</gender>
<disambiguation></disambiguation>
<genre>Pop/Rock</genre>
<style>Album Rock</style>
<style>Contemporary Pop/Rock</style>
<style>Singer/Songwriter</style>
<style>Soft Rock</style>
<style>Keyboard</style>
<mood>Amiable/Good-Natured</mood>
<mood>Autumnal</mood>
<mood>Nostalgic</mood>
<mood>Refined</mood>
<mood>Acerbic</mood>
<mood>Bittersweet</mood>
<mood>Brash</mood>
<mood>Cynical/Sarcastic</mood>
<mood>Earnest</mood>
<yearsactive>1960s - 2010s</yearsactive>
<born>1949-05-09</born>
<formed>1964</formed>
<biography>William Martin &quot;Billy&quot; Joel (born May 9, 1949, New York, USA) is an American pianist, singer-songwriter, and composer. Since releasing his first hit song, &quot;Piano Man&quot;, in 1973, Joel has become the sixth-best-selling recording artist and the third-best-selling solo artist in the United States, according to the RIAA. His compilation album Greatest Hits Vol. 1 &amp; 2 is the third-best-selling album in the United States by discs shipped.&#x0A;Joel had Top 40 hits in the 1970s, 1980s, and 1990s, achieving 33 Top 40 hits in the United States, all of which he wrote himself. He is also a six-time Grammy Award winner, a 23-time Grammy nominee and one of the world&apos;s best-selling artists of all time, having sold over 150 million records worldwide. He was inducted into the Songwriter&apos;s Hall of Fame (1992), the Rock and Roll Hall of Fame (1999), and the Long Island Music Hall of Fame (2006). In 2008, Billboard magazine released a list of the Hot 100 All-Time Top Artists to celebrate the US singles chart&apos;s 50th anniversary, with Billy Joel positioned at No. 23. With the exception of the 2007 songs &quot;All My Life&quot; and &quot;Christmas in Fallujah&quot;, Joel stopped writing and recording popular music after 1993&apos;s River of Dreams, but he continued to tour extensively until 2010. Joel was born in the Bronx, May 9, 1949 and raised in Hicksville, New York in a Levitt home. His father, Howard (born Helmuth), was born in Germany, the son of German merchant and manufacturer Karl Amson Joel, who, after the advent of the Nazi regime, emigrated to Switzerland and later to the United States. Billy Joel&apos;s mother, Rosalind Nyman, was born in England to Philip and Rebecca Nyman. Both of Joel&apos;s parents were Jewish. They divorced in 1960, and his father moved to Vienna, Austria. Billy has a sister, Judith Joel, and a half-brother, Alexander Joel, who is an acclaimed classical conductor in Europe and currently chief musical director of the Staatstheater Braunschweig.&#x0A;Joel&apos;s father was an accomplished classical pianist. Billy reluctantly began piano lessons at an early age, at his mother&apos;s insistence; his teachers included the noted American pianist Morton Estrin and musician/songwriter Timothy Ford. His interest in music, rather than sports, was a source of teasing and bullying in his early years. (He has said in interviews that his piano instructor also taught ballet. Her name was Frances Neiman, and she was a Juilliard trained musician. She gave both classical piano and ballet lessons in the studio attached to the rear of her house, leading neighborhood bullies to mistakenly assume that he was learning to dance.) As a teenager, Joel took up boxing so that he would be able to defend himself. He boxed successfully on the amateur Golden Gloves circuit for a short time, winning twenty-two bouts, but abandoned the sport shortly after breaking his nose in his twenty-fourth boxing match.&#x0A;Joel attended Hicksville High School in 1967, but he did not graduate with his class. He had been helping his single mother make ends meet by playing at a piano bar, which interfered with his school attendance. At the end of his senior year, Joel did not have enough credits to graduate. Rather than attend summer school to earn his diploma, however, Joel decided to immediately begin a career in music. Joel recounted, &quot;I told them, &apos;To hell with it. If I&apos;m not going to Columbia University, I&apos;m going to Columbia Records, and you don&apos;t need a high school diploma over there&apos;.&quot; Columbia did, in fact, become the label that eventually signed him. In 1992, he submitted essays to the school board and was awarded his diploma at Hicksville High&apos;s annual graduation ceremony, 25 years after he had left.</biography>
<died></died>
<disbanded></disbanded>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistthumb/joel-billy-541603848114c.jpg"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistthumb/joel-billy-541603848114c.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://www.theaudiodb.com/images/media/artist/thumb/ttsxwr1425765041.jpg/preview"">https://www.theaudiodb.com/images/media/artist/thumb/ttsxwr1425765041.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://rovimusic.rovicorp.com/image.jpg?c=73pC-Gp0OovlmiQL7Wp5Yd_M69_UI9rrJSVvWL2-yAg=&amp;f=2"">https://rovimusic.rovicorp.com/image.jpg?c=73pC-Gp0OovlmiQL7Wp5Yd_M69_UI9rrJSVvWL2-yAg=&amp;f=0</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://img.discogs.com/J3bqAiLmdr2gXsetNgSQF2W-f6M=/150x150/smart/filters:strip_icc():format(jpeg):mode_rgb():quality(40)/discogs-images/A-137418-1143052539.jpeg.jpg"">https://img.discogs.com/u7cfC3lZo9JGRdukSttJTZKr9Go=/350x255/smart/filters:strip_icc():format(jpeg):mode_rgb():quality(90)/discogs-images/A-137418-1143052539.jpeg.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview=""https://www.theaudiodb.com/images/media/artist/logo/tvqpys1367246337.png/preview"">https://www.theaudiodb.com/images/media/artist/logo/tvqpys1367246337.png</thumb>
<thumb spoof="""" cache="""" aspect=""clearart"" preview=""https://www.theaudiodb.com/images/media/artist/clearart/yqpsuq1523892204.png/preview"">https://www.theaudiodb.com/images/media/artist/clearart/yqpsuq1523892204.png</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview=""https://www.theaudiodb.com/images/media/artist/widethumb/tywpqx1530815867.jpg/preview"">https://www.theaudiodb.com/images/media/artist/widethumb/tywpqx1530815867.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""banner"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/musicbanner/joel-billy-5914e7759bfcd.jpg"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/musicbanner/joel-billy-5914e7759bfcd.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/hdmusiclogo/joel-billy-550b259604412.png"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/hdmusiclogo/joel-billy-550b259604412.png</thumb>
<thumb spoof="""" cache="""" aspect=""fanart"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistbackground/joel-billy-4fc0c2dad9ab7.jpg"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistbackground/joel-billy-4fc0c2dad9ab7.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""fanart"" preview=""https://www.theaudiodb.com/images/media/artist/fanart/uwqtup1521206367.jpg/preview"">https://www.theaudiodb.com/images/media/artist/fanart/uwqtup1521206367.jpg</thumb>
<path>F:\Music\ArtistInfoKodi\Billy Joel</path>
</artist>"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Name.Should().Be("Billy Joel");
nfo.Disambiguation.Should().BeNullOrEmpty();
nfo.Genres.Should().BeEquivalentTo(new List<string> { "Pop/Rock" });
nfo.Styles.Should().BeEquivalentTo(
new List<string>
{
"Album Rock",
"Contemporary Pop/Rock",
"Singer/Songwriter",
"Soft Rock",
"Keyboard"
});
nfo.Moods.Should().BeEquivalentTo(
new List<string>
{
"Amiable/Good-Natured",
"Autumnal",
"Nostalgic",
"Refined",
"Acerbic",
"Bittersweet",
"Brash",
"Cynical/Sarcastic",
"Earnest"
});
nfo.Biography.Should().Be(
@"William Martin ""Billy"" Joel (born May 9, 1949, New York, USA) is an American pianist, singer-songwriter, and composer. Since releasing his first hit song, ""Piano Man"", in 1973, Joel has become the sixth-best-selling recording artist and the third-best-selling solo artist in the United States, according to the RIAA. His compilation album Greatest Hits Vol. 1 & 2 is the third-best-selling album in the United States by discs shipped.
Joel had Top 40 hits in the 1970s, 1980s, and 1990s, achieving 33 Top 40 hits in the United States, all of which he wrote himself. He is also a six-time Grammy Award winner, a 23-time Grammy nominee and one of the world's best-selling artists of all time, having sold over 150 million records worldwide. He was inducted into the Songwriter's Hall of Fame (1992), the Rock and Roll Hall of Fame (1999), and the Long Island Music Hall of Fame (2006). In 2008, Billboard magazine released a list of the Hot 100 All-Time Top Artists to celebrate the US singles chart's 50th anniversary, with Billy Joel positioned at No. 23. With the exception of the 2007 songs ""All My Life"" and ""Christmas in Fallujah"", Joel stopped writing and recording popular music after 1993's River of Dreams, but he continued to tour extensively until 2010. Joel was born in the Bronx, May 9, 1949 and raised in Hicksville, New York in a Levitt home. His father, Howard (born Helmuth), was born in Germany, the son of German merchant and manufacturer Karl Amson Joel, who, after the advent of the Nazi regime, emigrated to Switzerland and later to the United States. Billy Joel's mother, Rosalind Nyman, was born in England to Philip and Rebecca Nyman. Both of Joel's parents were Jewish. They divorced in 1960, and his father moved to Vienna, Austria. Billy has a sister, Judith Joel, and a half-brother, Alexander Joel, who is an acclaimed classical conductor in Europe and currently chief musical director of the Staatstheater Braunschweig.
Joel's father was an accomplished classical pianist. Billy reluctantly began piano lessons at an early age, at his mother's insistence; his teachers included the noted American pianist Morton Estrin and musician/songwriter Timothy Ford. His interest in music, rather than sports, was a source of teasing and bullying in his early years. (He has said in interviews that his piano instructor also taught ballet. Her name was Frances Neiman, and she was a Juilliard trained musician. She gave both classical piano and ballet lessons in the studio attached to the rear of her house, leading neighborhood bullies to mistakenly assume that he was learning to dance.) As a teenager, Joel took up boxing so that he would be able to defend himself. He boxed successfully on the amateur Golden Gloves circuit for a short time, winning twenty-two bouts, but abandoned the sport shortly after breaking his nose in his twenty-fourth boxing match.
Joel attended Hicksville High School in 1967, but he did not graduate with his class. He had been helping his single mother make ends meet by playing at a piano bar, which interfered with his school attendance. At the end of his senior year, Joel did not have enough credits to graduate. Rather than attend summer school to earn his diploma, however, Joel decided to immediately begin a career in music. Joel recounted, ""I told them, 'To hell with it. If I'm not going to Columbia University, I'm going to Columbia Records, and you don't need a high school diploma over there'."" Columbia did, in fact, become the label that eventually signed him. In 1992, he submitted essays to the school board and was awarded his diploma at Hicksville High's annual graduation ceremony, 25 years after he had left.");
}
}
[Test]
public async Task MetadataNfo_With_Disambiguation_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(@"<artist><disambiguation>Test Disambiguation</disambiguation></artist>"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Disambiguation.Should().Be("Test Disambiguation");
}
}
}

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

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
@ -8,10 +10,14 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo; @@ -8,10 +10,14 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class EpisodeNfoReaderTests
{
[SetUp]
public void SetUp() => _episodeNfoReader = new EpisodeNfoReader(new Mock<IClient>().Object);
private EpisodeNfoReader _episodeNfoReader;
[Test]
public async Task One()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -19,15 +25,18 @@ public class EpisodeNfoReaderTests @@ -19,15 +25,18 @@ public class EpisodeNfoReaderTests
<episodedetails>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(1);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
}
}
[Test]
public async Task Two()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -45,19 +54,22 @@ public class EpisodeNfoReaderTests @@ -45,19 +54,22 @@ public class EpisodeNfoReaderTests
<season>1</season>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(2);
result.All(nfo => nfo.ShowTitle == "show").Should().BeTrue();
result.All(nfo => nfo.Season == 1).Should().BeTrue();
result.Count(nfo => nfo.Title == "episode-one" && nfo.Episode == 1).Should().Be(1);
result.Count(nfo => nfo.Title == "episode-two" && nfo.Episode == 2).Should().Be(1);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.All(nfo => nfo.ShowTitle == "show").Should().BeTrue();
list.All(nfo => nfo.Season == 1).Should().BeTrue();
list.Count(nfo => nfo.Title == "episode-one" && nfo.Episode == 1).Should().Be(1);
list.Count(nfo => nfo.Title == "episode-two" && nfo.Episode == 2).Should().Be(1);
}
}
[Test]
public async Task UniqueIds()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -67,18 +79,21 @@ public class EpisodeNfoReaderTests @@ -67,18 +79,21 @@ public class EpisodeNfoReaderTests
<uniqueid default=""false"" type=""imdb"">tt54321</uniqueid>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(1);
result[0].UniqueIds.Count.Should().Be(2);
result[0].UniqueIds.Count(id => id.Default && id.Type == "tvdb" && id.Guid == "12345").Should().Be(1);
result[0].UniqueIds.Count(id => !id.Default && id.Type == "imdb" && id.Guid == "tt54321").Should().Be(1);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].UniqueIds.Count.Should().Be(2);
list[0].UniqueIds.Count(id => id.Default && id.Type == "tvdb" && id.Guid == "12345").Should().Be(1);
list[0].UniqueIds.Count(id => !id.Default && id.Type == "imdb" && id.Guid == "tt54321").Should().Be(1);
}
}
[Test]
public async Task No_ContentRating()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -87,16 +102,19 @@ public class EpisodeNfoReaderTests @@ -87,16 +102,19 @@ public class EpisodeNfoReaderTests
<mpaa/>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(1);
result[0].ContentRating.Should().BeNullOrEmpty();
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].ContentRating.Should().BeNullOrEmpty();
}
}
[Test]
public async Task ContentRating()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -108,17 +126,20 @@ public class EpisodeNfoReaderTests @@ -108,17 +126,20 @@ public class EpisodeNfoReaderTests
<mpaa>US:Something / US:SomethingElse</mpaa>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(2);
result.Count(nfo => nfo.ContentRating == "US:Something").Should().Be(1);
result.Count(nfo => nfo.ContentRating == "US:Something / US:SomethingElse").Should().Be(1);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.ContentRating == "US:Something").Should().Be(1);
list.Count(nfo => nfo.ContentRating == "US:Something / US:SomethingElse").Should().Be(1);
}
}
[Test]
public async Task No_Plot()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -127,16 +148,19 @@ public class EpisodeNfoReaderTests @@ -127,16 +148,19 @@ public class EpisodeNfoReaderTests
<plot/>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(1);
result[0].Plot.Should().BeNullOrEmpty();
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Plot.Should().BeNullOrEmpty();
}
}
[Test]
public async Task Plot()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -145,16 +169,19 @@ public class EpisodeNfoReaderTests @@ -145,16 +169,19 @@ public class EpisodeNfoReaderTests
<plot>Some Plot</plot>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(1);
result[0].Plot.Should().Be("Some Plot");
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Plot.Should().Be("Some Plot");
}
}
[Test]
public async Task Actors()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -172,20 +199,23 @@ public class EpisodeNfoReaderTests @@ -172,20 +199,23 @@ public class EpisodeNfoReaderTests
</actor>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(1);
result[0].Actors.Count.Should().Be(2);
result[0].Actors.Count(a => a.Name == "Name 1" && a.Role == "Role 1" && a.Thumb == "Thumb 1")
.Should().Be(1);
result[0].Actors.Count(a => a.Name == "Name 2" && a.Role == "Role 2" && a.Thumb == "Thumb 2")
.Should().Be(1);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Actors.Count.Should().Be(2);
list[0].Actors.Count(a => a.Name == "Name 1" && a.Role == "Role 1" && a.Thumb == "Thumb 1")
.Should().Be(1);
list[0].Actors.Count(a => a.Name == "Name 2" && a.Role == "Role 2" && a.Thumb == "Thumb 2")
.Should().Be(1);
}
}
[Test]
public async Task Writers()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -198,18 +228,21 @@ public class EpisodeNfoReaderTests @@ -198,18 +228,21 @@ public class EpisodeNfoReaderTests
<credits>Writer 3</credits>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.Count.Should().Be(2);
result.Count(nfo => nfo.Writers.Count == 1 && nfo.Writers[0] == "Writer 1").Should().Be(1);
result.Count(nfo => nfo.Writers.Count == 2 && nfo.Writers[0] == "Writer 2" && nfo.Writers[1] == "Writer 3")
.Should().Be(1);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.Writers.Count == 1 && nfo.Writers[0] == "Writer 1").Should().Be(1);
list.Count(nfo => nfo.Writers.Count == 2 && nfo.Writers[0] == "Writer 2" && nfo.Writers[1] == "Writer 3")
.Should().Be(1);
}
}
[Test]
public async Task Directors()
{
var reader = new EpisodeNfoReader();
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
@ -222,13 +255,153 @@ public class EpisodeNfoReaderTests @@ -222,13 +255,153 @@ public class EpisodeNfoReaderTests
<director>Director 3</director>
</episodedetails>"));
List<TvShowEpisodeNfo> result = await reader.Read(stream);
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.Directors.Count == 1 && nfo.Directors[0] == "Director 1").Should().Be(1);
list.Count(
nfo => nfo.Directors.Count == 2 && nfo.Directors[0] == "Director 2" &&
nfo.Directors[1] == "Director 3")
.Should().Be(1);
}
}
[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"" ?>
<episodedetails>
<title>Filmed Before a Live Studio Audience</title>
<showtitle>WandaVision</showtitle>
<ratings>
<rating name=""imdb"" max=""10"" default=""true"">
<value>7.500000</value>
<votes>18766</votes>
</rating>
<rating name=""tmdb"" max=""10"">
<value>7.500000</value>
<votes>42</votes>
</rating>
<rating name=""trakt"" max=""10"">
<value>6.952780</value>
<votes>3621</votes>
</rating>
</ratings>
<userrating>0</userrating>
<top250>0</top250>
<season>1</season>
<episode>1</episode>
<displayseason>-1</displayseason>
<displayepisode>-1</displayepisode>
<outline></outline>
<plot>Wanda and Vision struggle to conceal their powers during dinner with Visions boss and his wife.</plot>
<tagline></tagline>
<runtime>26</runtime>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://image.tmdb.org/t/p/w780/cbe8l0Hnbvu07ePgoOopyWYrcdL.jpg"">https://image.tmdb.org/t/p/original/cbe8l0Hnbvu07ePgoOopyWYrcdL.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://image.tmdb.org/t/p/w780/oNCzeCXFanVEWNpzRzyffhLLfZs.jpg"">https://image.tmdb.org/t/p/original/oNCzeCXFanVEWNpzRzyffhLLfZs.jpg</thumb>
<mpaa>Australia:TV-14</mpaa>
<playcount>1</playcount>
<lastplayed>2021-03-27</lastplayed>
<id>1830976</id>
<uniqueid type=""imdb"">tt9601584</uniqueid>
<uniqueid type=""tmdb"" default=""true"">1830976</uniqueid>
<uniqueid type=""tvdb"">8042515</uniqueid>
<genre>Sci-Fi &amp; Fantasy</genre>
<genre>Mystery</genre>
<genre>Drama</genre>
<credits>Jac Schaeffer</credits>
<director>Matt Shakman</director>
<premiered>2021-01-15</premiered>
<year>2021</year>
<status></status>
<code></code>
<aired>2021-01-15</aired>
<studio>Disney+ (US)</studio>
<trailer></trailer>
<fileinfo>
<streamdetails>
<video>
<codec>h264</codec>
<aspect>1.777778</aspect>
<width>1280</width>
<height>720</height>
<durationinseconds>1593</durationinseconds>
<stereomode></stereomode>
</video>
<audio>
<codec>aac</codec>
<language>eng</language>
<channels>2</channels>
</audio>
</streamdetails>
</fileinfo>
<actor>
<name>Randall Park</name>
<role>Jimmy Woo</role>
<order>4</order>
<thumb>https://image.tmdb.org/t/p/original/1QJ4cBQZoOaLR8Hc3V2NgBLvB0f.jpg</thumb>
</actor>
<actor>
<name>Kat Dennings</name>
<role>Darcy Lewis / The Escape Artist</role>
<order>5</order>
<thumb>https://image.tmdb.org/t/p/original/rrfyo9z1wW5nY9ZsFlj1Ozfj9g2.jpg</thumb>
</actor>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2021-02-02 11:57:44</dateadded>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowEpisodeNfo nfo in result.RightToSeq().Flatten())
{
nfo.ShowTitle.Should().Be("WandaVision");
nfo.Title.Should().Be("Filmed Before a Live Studio Audience");
nfo.Episode.Should().Be(1);
nfo.Season.Should().Be(1);
nfo.ContentRating.Should().Be("Australia:TV-14");
nfo.Aired.IsSome.Should().BeTrue();
foreach (DateTime aired in nfo.Aired)
{
aired.Should().Be(new DateTime(2021, 01, 15));
}
result.Count.Should().Be(2);
result.Count(nfo => nfo.Directors.Count == 1 && nfo.Directors[0] == "Director 1").Should().Be(1);
result.Count(
nfo => nfo.Directors.Count == 2 && nfo.Directors[0] == "Director 2" &&
nfo.Directors[1] == "Director 3")
.Should().Be(1);
nfo.Plot.Should().Be(
"Wanda and Vision struggle to conceal their powers during dinner with Vision’s boss and his wife.");
nfo.Actors.Should().BeEquivalentTo(
new List<ActorNfo>
{
new()
{
Name = "Randall Park", Order = 4, Role = "Jimmy Woo",
Thumb = "https://image.tmdb.org/t/p/original/1QJ4cBQZoOaLR8Hc3V2NgBLvB0f.jpg"
},
new()
{
Name = "Kat Dennings", Order = 5, Role = "Darcy Lewis / The Escape Artist",
Thumb = "https://image.tmdb.org/t/p/original/rrfyo9z1wW5nY9ZsFlj1Ozfj9g2.jpg"
}
});
nfo.Writers.Should().BeEquivalentTo(new List<string> { "Jac Schaeffer" });
nfo.Directors.Should().BeEquivalentTo(new List<string> { "Matt Shakman" });
nfo.UniqueIds.Should().BeEquivalentTo(
new List<UniqueIdNfo>
{
new() { Type = "imdb", Guid = "tt9601584", Default = false },
new() { Type = "tmdb", Guid = "1830976", Default = true },
new() { Type = "tvdb", Guid = "8042515", Default = false }
});
}
}
}

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

@ -173,7 +173,13 @@ https://www.themoviedb.org/movie/11-star-wars")); @@ -173,7 +173,13 @@ https://www.themoviedb.org/movie/11-star-wars"));
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.Premiered.IsSome.Should().BeTrue();
foreach (DateTime premiered in nfo.Premiered)
{
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();

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

@ -0,0 +1,156 @@ @@ -0,0 +1,156 @@
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 MusicVideoNfoReaderTests
{
[SetUp]
public void SetUp() => _musicVideoNfoReader = new MusicVideoNfoReader(new Mock<IClient>().Object);
private MusicVideoNfoReader _musicVideoNfoReader;
[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, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<musicvideo></musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<musicvideo></musicvideo>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.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"" ?>
<musicvideo>
<title>Dancing Queen</title>
<userrating>0</userrating>
<top250>0</top250>
<track>-1</track>
<album>Arrival</album>
<outline></outline>
<plot>Dancing Queen est un des tubes emblématiques de l&apos;ère disco produits par le groupe suédois ABBA en 1976. Ce tube connaît un regain de popularité en 1994 lors de la sortie de Priscilla, folle du désert, et fait « presque » partie de la distribution du film Muriel.&#x0A;Le groupe a également enregistré une version espagnole de ce titre, La reina del baile, pour le marché d&apos;Amérique latine. On peut retrouver ces versions en espagnol des succès de ABBA sur l&apos;abum Oro. Le 18 juin 1976, ABBA a interprété cette chanson lors d&apos;un spectacle télévisé organisé en l&apos;honneur du roi Charles XVI Gustave de Suède, qui venait de se marier. Le titre sera repris en 2011 par Glee dans la saison 2, épisode 20.</plot>
<tagline></tagline>
<runtime>2</runtime>
<thumb preview=""https://www.theaudiodb.com/images/media/album/thumb/arrival-4ee244732bbde.jpg/preview"">https://www.theaudiodb.com/images/media/album/thumb/arrival-4ee244732bbde.jpg</thumb>
<thumb preview=""https://assets.fanart.tv/preview/music/d87e52c5-bb8d-4da8-b941-9f4928627dc8/albumcover/arrival-548ab7a698b49.jpg"">https://assets.fanart.tv/fanart/music/d87e52c5-bb8d-4da8-b941-9f4928627dc8/albumcover/arrival-548ab7a698b49.jpg</thumb>
<mpaa></mpaa>
<playcount>0</playcount>
<lastplayed></lastplayed>
<id></id>
<genre>Pop</genre>
<year>1976</year>
<status></status>
<director>Director 1</director>
<director>Director 2</director>
<director>Director 3</director>
<director>Director 4</director>
<code></code>
<aired></aired>
<trailer></trailer>
<fileinfo>
<streamdetails>
<video>
<codec>hevc</codec>
<aspect>1.792230</aspect>
<width>716</width>
<height>568</height>
<durationinseconds>143</durationinseconds>
<stereomode></stereomode>
</video>
<audio>
<codec>ac3</codec>
<language>eng</language>
<channels>2</channels>
</audio>
</streamdetails>
</fileinfo>
<artist>ABBA</artist>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2018-09-10 09:46:06</dateadded>
</musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MusicVideoNfo nfo in result.RightToSeq())
{
nfo.Artist.Should().Be("ABBA");
nfo.Title.Should().Be("Dancing Queen");
nfo.Album.Should().Be("Arrival");
nfo.Plot.Should().Be(
@"Dancing Queen est un des tubes emblématiques de l'ère disco produits par le groupe suédois ABBA en 1976. Ce tube connaît un regain de popularité en 1994 lors de la sortie de Priscilla, folle du désert, et fait « presque » partie de la distribution du film Muriel.
Le groupe a également enregistré une version espagnole de ce titre, La reina del baile, pour le marché d'Amérique latine. On peut retrouver ces versions en espagnol des succès de ABBA sur l'abum Oro. Le 18 juin 1976, ABBA a interprété cette chanson lors d'un spectacle télévisé organisé en l'honneur du roi Charles XVI Gustave de Suède, qui venait de se marier. Le titre sera repris en 2011 par Glee dans la saison 2, épisode 20.");
nfo.Year.Should().Be(1976);
nfo.Genres.Should().BeEquivalentTo(new List<string> { "Pop" });
}
}
[Test]
public async Task MetadataNfo_With_Tags_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(@"<musicvideo><tag>Test Tag</tag></musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MusicVideoNfo nfo in result.RightToSeq())
{
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
}
}
[Test]
public async Task MetadataNfo_With_Studios_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(@"<musicvideo><studio>Test Studio</studio></musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MusicVideoNfo nfo in result.RightToSeq())
{
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Test Studio" });
}
}
}

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

@ -0,0 +1,216 @@ @@ -0,0 +1,216 @@
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 TvShowNfoReaderTests
{
[SetUp]
public void SetUp() => _tvShowNfoReader = new TvShowNfoReader(new Mock<IClient>().Object);
private TvShowNfoReader _tvShowNfoReader;
[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, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<tvshow></tvshow>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.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"" ?>
<tvshow>
<title>WandaVision</title>
<originaltitle>WandaVision</originaltitle>
<showtitle>WandaVision</showtitle>
<ratings>
<rating name=""imdb"" max=""10"" default=""true"">
<value>8.200000</value>
<votes>105359</votes>
</rating>
<rating name=""tmdb"" max=""10"">
<value>8.500000</value>
<votes>7230</votes>
</rating>
<rating name=""trakt"" max=""10"">
<value>8.077950</value>
<votes>3284</votes>
</rating>
</ratings>
<userrating>0</userrating>
<top250>0</top250>
<season>1</season>
<episode>9</episode>
<displayseason>-1</displayseason>
<displayepisode>-1</displayepisode>
<outline></outline>
<plot>Wanda Maximoff and Visiontwo super-powered beings living idealized suburban livesbegin to suspect that everything is not as it seems.</plot>
<tagline></tagline>
<runtime>0</runtime>
<thumb spoof="""" cache="""" aspect=""landscape"" preview=""https://image.tmdb.org/t/p/w780/dUWto4NaeJFrGx7jm8m3KLymUGf.jpg"">https://image.tmdb.org/t/p/original/dUWto4NaeJFrGx7jm8m3KLymUGf.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""poster"" preview=""https://image.tmdb.org/t/p/w780/8UsAB1hgwnd80eI2ociyppB6UXL.jpg"">https://image.tmdb.org/t/p/original/8UsAB1hgwnd80eI2ociyppB6UXL.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""poster"" preview=""https://assets.fanart.tv/preview/tv/362392/tvposter/wandavision-6009571d1ed1f.jpg"">https://assets.fanart.tv/fanart/tv/362392/tvposter/wandavision-6009571d1ed1f.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview=""https://assets.fanart.tv/preview/tv/362392/hdtvlogo/marvels-wandavision-5f6ac3b1e9458.png"">https://assets.fanart.tv/fanart/tv/362392/hdtvlogo/marvels-wandavision-5f6ac3b1e9458.png</thumb>
<thumb spoof="""" cache="""" aspect=""clearart"" preview=""https://assets.fanart.tv/preview/tv/362392/hdclearart/wandavision-6009b6875a285.png"">https://assets.fanart.tv/fanart/tv/362392/hdclearart/wandavision-6009b6875a285.png</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview=""https://assets.fanart.tv/preview/tv/362392/tvthumb/wandavision-603032a5349b9.jpg"">https://assets.fanart.tv/fanart/tv/362392/tvthumb/wandavision-603032a5349b9.jpg</thumb>
<thumb spoof="""" cache="""" season=""1"" type=""season"" aspect=""poster"" preview=""https://image.tmdb.org/t/p/w780/7u443QI5xNIfLgNzEsV43CYZCWX.jpg"">https://image.tmdb.org/t/p/original/7u443QI5xNIfLgNzEsV43CYZCWX.jpg</thumb>
<fanart>
<thumb colors="""" preview="""">https://image.tmdb.org/t/p/original/57vVjteucIF3bGnZj6PmaoJRScw.jpg</thumb>
<thumb colors="""" preview="""">https://assets.fanart.tv/fanart/tv/362392/showbackground/marvels-wandavision-5ff4fef387a43.jpg</thumb>
</fanart>
<mpaa>Australia:M</mpaa>
<playcount>0</playcount>
<lastplayed>2021-03-29</lastplayed>
<id>85271</id>
<uniqueid type=""imdb"">tt9140560</uniqueid>
<uniqueid type=""tmdb"" default=""true"">85271</uniqueid>
<uniqueid type=""tvdb"">362392</uniqueid>
<genre>SuperHero</genre>
<premiered>2021-01-15</premiered>
<year>2021</year>
<status>Ended</status>
<code></code>
<aired></aired>
<studio>Disney+</studio>
<actor>
<name>Elizabeth Olsen</name>
<role>Wanda Maximoff / The Scarlet Witch</role>
<order>0</order>
<thumb>https://image.tmdb.org/t/p/original/wIU675y4dofIDVuhaNWPizJNtep.jpg</thumb>
</actor>
<actor>
<name>Paul Bettany</name>
<role>Vision / The Vision</role>
<order>1</order>
<thumb>https://image.tmdb.org/t/p/original/vcAVrAOZrpqmi37qjFdztRAv1u9.jpg</thumb>
</actor>
<namedseason number=""1"">Season 1</namedseason>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2021-03-12 06:15:51</dateadded>
</tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Title.Should().Be("WandaVision");
nfo.Year.Should().Be(2021);
nfo.Plot.Should().Be(
"Wanda Maximoff and Vision—two super-powered beings living idealized suburban lives—begin to suspect that everything is not as it seems.");
nfo.ContentRating.Should().Be("Australia:M");
nfo.Genres.Should().BeEquivalentTo(new List<string> { "SuperHero" });
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Disney+" });
nfo.Actors.Should().BeEquivalentTo(
new List<ActorNfo>
{
new()
{
Name = "Elizabeth Olsen", Order = 0, Role = "Wanda Maximoff / The Scarlet Witch",
Thumb = "https://image.tmdb.org/t/p/original/wIU675y4dofIDVuhaNWPizJNtep.jpg"
},
new()
{
Name = "Paul Bettany", Order = 1, Role = "Vision / The Vision",
Thumb = "https://image.tmdb.org/t/p/original/vcAVrAOZrpqmi37qjFdztRAv1u9.jpg"
}
});
nfo.UniqueIds.Should().BeEquivalentTo(
new List<UniqueIdNfo>
{
new() { Type = "imdb", Guid = "tt9140560", Default = false },
new() { Type = "tmdb", Guid = "85271", Default = true },
new() { Type = "tvdb", Guid = "362392", Default = false }
});
nfo.Premiered.IsSome.Should().BeTrue();
foreach (DateTime premiered in nfo.Premiered)
{
premiered.Should().Be(new DateTime(2021, 1, 15));
}
}
}
[Test]
public async Task MetadataNfo_With_Outline_Should_Return_Nfo()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><outline>Test Outline</outline></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Outline.Should().Be("Test Outline");
}
}
[Test]
public async Task MetadataNfo_With_Tagline_Should_Return_Nfo()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><tagline>Test Tagline</tagline></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Tagline.Should().Be("Test Tagline");
}
}
[Test]
public async Task MetadataNfo_With_Tag_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><tag>Test Tag</tag></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
}
}
}

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

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

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<List<TvShowEpisodeNfo>> Read(Stream input);
Task<Either<BaseError, List<TvShowEpisodeNfo>>> Read(Stream input);
}

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

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

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

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

116
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using System.Xml.Serialization;
using Bugsnag;
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
@ -12,9 +11,7 @@ namespace ErsatzTV.Core.Metadata; @@ -12,9 +11,7 @@ namespace ErsatzTV.Core.Metadata;
public class LocalMetadataProvider : ILocalMetadataProvider
{
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowNfo));
private static readonly XmlSerializer ArtistSerializer = new(typeof(ArtistNfo));
private static readonly XmlSerializer MusicVideoSerializer = new(typeof(MusicVideoNfo));
private readonly IArtistNfoReader _artistNfoReader;
private readonly IArtistRepository _artistRepository;
private readonly IClient _client;
private readonly IEpisodeNfoReader _episodeNfoReader;
@ -22,14 +19,15 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -22,14 +19,15 @@ public class LocalMetadataProvider : ILocalMetadataProvider
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger<LocalMetadataProvider> _logger;
private readonly IMetadataRepository _metadataRepository;
private readonly IMovieNfoReader _movieNfoReader;
private readonly IMovieRepository _movieRepository;
private readonly IMusicVideoNfoReader _musicVideoNfoReader;
private readonly IMusicVideoRepository _musicVideoRepository;
private readonly IOtherVideoRepository _otherVideoRepository;
private readonly ISongRepository _songRepository;
private readonly ITelevisionRepository _televisionRepository;
private readonly ITvShowNfoReader _tvShowNfoReader;
public LocalMetadataProvider(
IMetadataRepository metadataRepository,
@ -43,6 +41,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -43,6 +41,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider
ILocalFileSystem localFileSystem,
IMovieNfoReader movieNfoReader,
IEpisodeNfoReader episodeNfoReader,
IArtistNfoReader artistNfoReader,
IMusicVideoNfoReader musicVideoNfoReader,
ITvShowNfoReader tvShowNfoReader,
ILocalStatisticsProvider localStatisticsProvider,
IClient client,
ILogger<LocalMetadataProvider> logger)
@ -58,6 +59,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -58,6 +59,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider
_localFileSystem = localFileSystem;
_movieNfoReader = movieNfoReader;
_episodeNfoReader = episodeNfoReader;
_artistNfoReader = artistNfoReader;
_musicVideoNfoReader = musicVideoNfoReader;
_tvShowNfoReader = tvShowNfoReader;
_localStatisticsProvider = localStatisticsProvider;
_client = client;
_logger = logger;
@ -168,8 +172,16 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -168,8 +172,16 @@ public class LocalMetadataProvider : ILocalMetadataProvider
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<MusicVideoNfo> maybeNfo = MusicVideoSerializer.Deserialize(fileStream) as MusicVideoNfo;
foreach (MusicVideoNfo nfo in maybeNfo)
Either<BaseError, MusicVideoNfo> maybeNfo = await _musicVideoNfoReader.Read(fileStream);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
"Failed to read MusicVideo nfo metadata from {Path}: {Error}",
nfoFileName,
error.ToString());
}
foreach (MusicVideoNfo nfo in maybeNfo.RightToSeq())
{
return new MusicVideoMetadata
{
@ -179,8 +191,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -179,8 +191,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider
Album = nfo.Album,
Title = nfo.Title,
Plot = nfo.Plot,
Year = GetYear(nfo.Year, nfo.Premiered),
ReleaseDate = GetAired(nfo.Year, nfo.Premiered),
Year = GetYear(nfo.Year, string.Empty),
ReleaseDate = GetAired(nfo.Year, string.Empty),
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList()
@ -815,8 +827,16 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -815,8 +827,16 @@ public class LocalMetadataProvider : ILocalMetadataProvider
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<TvShowNfo> maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowNfo;
foreach (TvShowNfo nfo in maybeNfo)
Either<BaseError, TvShowNfo> maybeNfo = await _tvShowNfoReader.Read(fileStream);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
"Failed to read TvShow nfo metadata from {Path}: {Error}",
nfoFileName,
error.ToString());
}
foreach (TvShowNfo nfo in maybeNfo.RightToSeq())
{
DateTime dateAdded = DateTime.UtcNow;
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
@ -858,8 +878,16 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -858,8 +878,16 @@ public class LocalMetadataProvider : ILocalMetadataProvider
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<ArtistNfo> maybeNfo = ArtistSerializer.Deserialize(fileStream) as ArtistNfo;
foreach (ArtistNfo nfo in maybeNfo)
Either<BaseError, ArtistNfo> maybeNfo = await _artistNfoReader.Read(fileStream);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
"Failed to read Artist nfo metadata from {Path}: {Error}",
nfoFileName,
error.ToString());
}
foreach (ArtistNfo nfo in maybeNfo.RightToSeq())
{
return new ArtistMetadata
{
@ -890,9 +918,17 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -890,9 +918,17 @@ public class LocalMetadataProvider : ILocalMetadataProvider
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
List<TvShowEpisodeNfo> nfos = await _episodeNfoReader.Read(fileStream);
Either<BaseError, List<TvShowEpisodeNfo>> maybeNfo = await _episodeNfoReader.Read(fileStream);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
"Failed to read Episode nfo metadata from {Path}: {Error}",
nfoFileName,
error.ToString());
}
var result = new List<EpisodeMetadata>();
foreach (TvShowEpisodeNfo nfo in nfos)
foreach (TvShowEpisodeNfo nfo in maybeNfo.RightToSeq().Flatten())
{
DateTime dateAdded = DateTime.UtcNow;
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
@ -954,10 +990,23 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -954,10 +990,23 @@ public class LocalMetadataProvider : ILocalMetadataProvider
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;
var year = 0;
if (nfo.Year > 1000)
{
year = nfo.Year;
}
DateTime releaseDate = new DateTimeOffset(year, 0, 0, 0, 0, 0, TimeSpan.Zero).UtcDateTime;
foreach (DateTime premiered in nfo.Premiered)
{
if (year == 0)
{
year = premiered.Year;
}
releaseDate = premiered;
}
return new MovieMetadata
{
@ -1014,6 +1063,21 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1014,6 +1063,21 @@ public class LocalMetadataProvider : ILocalMetadataProvider
return null;
}
private static int? GetYear(int? year, Option<DateTime> premiered)
{
if (year is > 1000)
{
return year;
}
foreach (DateTime p in premiered)
{
return p.Year;
}
return null;
}
private static DateTime? GetAired(int? year, string aired)
{
DateTime? fallback = year is > 1000 ? new DateTime(year.Value, 1, 1) : null;
@ -1026,6 +1090,18 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1026,6 +1090,18 @@ public class LocalMetadataProvider : ILocalMetadataProvider
return DateTime.TryParse(aired, out DateTime parsed) ? parsed : fallback;
}
private static DateTime? GetAired(int? year, Option<DateTime> aired)
{
DateTime? fallback = year is > 1000 ? new DateTime(year.Value, 1, 1) : null;
foreach (DateTime a in aired)
{
return a;
}
return fallback;
}
private async Task<bool> UpdateMetadataCollections<T>(
T existing,
T incoming,

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

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
using System.Xml;
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
namespace ErsatzTV.Core.Metadata.Nfo;
public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader
{
private readonly IClient _client;
public ArtistNfoReader(IClient client) => _client = client;
public async Task<Either<BaseError, ArtistNfo>> Read(Stream input)
{
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())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
switch (reader.Name.ToLowerInvariant())
{
case "artist":
nfo = new ArtistNfo
{
Genres = new List<string>(),
Styles = new List<string>(),
Moods = new List<string>()
};
break;
case "name":
await ReadStringContent(reader, nfo, (artist, name) => artist.Name = name);
break;
case "disambiguation":
await ReadStringContent(
reader,
nfo,
(artist, disambiguation) => artist.Disambiguation = disambiguation);
break;
case "genre":
await ReadStringContent(reader, nfo, (artist, genre) => artist.Genres.Add(genre));
break;
case "style":
await ReadStringContent(reader, nfo, (artist, style) => artist.Styles.Add(style));
break;
case "mood":
await ReadStringContent(reader, nfo, (artist, mood) => artist.Moods.Add(mood));
break;
case "biography":
await ReadStringContent(
reader,
nfo,
(artist, biography) => artist.Biography = biography);
break;
}
break;
case XmlNodeType.EndElement:
if (reader.Name == "artist")
{
done = true;
}
break;
}
}
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);
return new FailedToReadNfo(ex.ToString());
}
}
}

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

@ -1,104 +1,115 @@ @@ -1,104 +1,115 @@
using System.Xml;
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
namespace ErsatzTV.Core.Metadata.Nfo;
public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
{
public async Task<List<TvShowEpisodeNfo>> Read(Stream input)
{
var result = new List<TvShowEpisodeNfo>();
private readonly IClient _client;
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
TvShowEpisodeNfo nfo = null;
public EpisodeNfoReader(IClient client) => _client = client;
while (await reader.ReadAsync())
public async Task<Either<BaseError, List<TvShowEpisodeNfo>>> Read(Stream input)
{
try
{
switch (reader.NodeType)
var result = new List<TvShowEpisodeNfo>();
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings);
TvShowEpisodeNfo nfo = null;
while (await reader.ReadAsync())
{
case XmlNodeType.Element:
switch (reader.Name.ToLowerInvariant())
{
case "episodedetails":
nfo = new TvShowEpisodeNfo
{
UniqueIds = new List<UniqueIdNfo>(),
Actors = new List<ActorNfo>(),
Writers = new List<string>(),
Directors = new List<string>()
};
break;
case "title":
await ReadStringContent(reader, nfo, (episode, title) => episode.Title = title);
break;
case "showtitle":
await ReadStringContent(reader, nfo, (episode, showTitle) => episode.ShowTitle = showTitle);
break;
case "episode":
await ReadIntContent(
reader,
nfo,
(episode, episodeNumber) => episode.Episode = episodeNumber);
break;
case "season":
await ReadIntContent(reader, nfo, (episode, seasonNumber) => episode.Season = seasonNumber);
break;
case "uniqueid":
await ReadUniqueId(reader, nfo, (episode, uniqueId) => episode.UniqueIds.Add(uniqueId));
break;
case "mpaa":
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 ReadStringContent(reader, nfo, (episode, plot) => episode.Plot = plot);
break;
case "actor":
ReadActor(reader, nfo, (episode, actor) => episode.Actors.Add(actor));
break;
case "credits":
await ReadStringContent(reader, nfo, (episode, writer) => episode.Writers.Add(writer));
break;
case "director":
await ReadStringContent(
reader,
nfo,
(episode, director) => episode.Directors.Add(director));
break;
}
switch (reader.NodeType)
{
case XmlNodeType.Element:
switch (reader.Name.ToLowerInvariant())
{
case "episodedetails":
nfo = new TvShowEpisodeNfo
{
UniqueIds = new List<UniqueIdNfo>(),
Actors = new List<ActorNfo>(),
Writers = new List<string>(),
Directors = new List<string>()
};
break;
case "title":
await ReadStringContent(reader, nfo, (episode, title) => episode.Title = title);
break;
case "showtitle":
await ReadStringContent(
reader,
nfo,
(episode, showTitle) => episode.ShowTitle = showTitle);
break;
case "episode":
await ReadIntContent(
reader,
nfo,
(episode, episodeNumber) => episode.Episode = episodeNumber);
break;
case "season":
await ReadIntContent(
reader,
nfo,
(episode, seasonNumber) => episode.Season = seasonNumber);
break;
case "uniqueid":
await ReadUniqueId(reader, nfo, (episode, uniqueId) => episode.UniqueIds.Add(uniqueId));
break;
case "mpaa":
await ReadStringContent(
reader,
nfo,
(episode, contentRating) => episode.ContentRating = contentRating);
break;
case "aired":
await ReadDateTimeContent(reader, nfo, (episode, aired) => episode.Aired = aired);
break;
case "plot":
await ReadStringContent(reader, nfo, (episode, plot) => episode.Plot = plot);
break;
case "actor":
ReadActor(reader, nfo, (episode, actor) => episode.Actors.Add(actor));
break;
case "credits":
await ReadStringContent(reader, nfo, (episode, writer) => episode.Writers.Add(writer));
break;
case "director":
await ReadStringContent(
reader,
nfo,
(episode, director) => episode.Directors.Add(director));
break;
}
break;
case XmlNodeType.EndElement:
switch (reader.Name.ToLowerInvariant())
{
case "episodedetails":
if (nfo != null)
{
result.Add(nfo);
}
break;
case XmlNodeType.EndElement:
switch (reader.Name.ToLowerInvariant())
{
case "episodedetails":
if (nfo != null)
{
result.Add(nfo);
}
break;
}
break;
}
break;
break;
}
}
}
return result;
}
private static async Task ReadAired(XmlReader reader, TvShowEpisodeNfo nfo)
{
if (nfo != null)
return result;
}
catch (Exception ex)
{
nfo.Aired = await reader.ReadElementContentAsStringAsync();
_client.Notify(ex);
return new FailedToReadNfo(ex.ToString());
}
}
}

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

@ -21,7 +21,7 @@ public class MovieNfo @@ -21,7 +21,7 @@ public class MovieNfo
public string ContentRating { get; set; }
[XmlElement("premiered")]
public DateTime Premiered { get; set; }
public Option<DateTime> Premiered { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }

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

@ -17,9 +17,6 @@ public class MusicVideoNfo @@ -17,9 +17,6 @@ public class MusicVideoNfo
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("premiered")]
public string Premiered { get; set; }
[XmlElement("year")]
public int Year { get; set; }

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

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
using System.Xml;
using Bugsnag;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
namespace ErsatzTV.Core.Metadata.Nfo;
public class MusicVideoNfoReader : NfoReader<MusicVideoNfo>, IMusicVideoNfoReader
{
private readonly IClient _client;
public MusicVideoNfoReader(IClient client) => _client = client;
public async Task<Either<BaseError, MusicVideoNfo>> Read(Stream input)
{
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())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
switch (reader.Name.ToLowerInvariant())
{
case "musicvideo":
nfo = new MusicVideoNfo
{
Genres = new List<string>(),
Tags = new List<string>(),
Studios = new List<string>()
};
break;
case "artist":
await ReadStringContent(
reader,
nfo,
(musicVideo, artist) => musicVideo.Artist = artist);
break;
case "title":
await ReadStringContent(reader, nfo, (musicVideo, title) => musicVideo.Title = title);
break;
case "album":
await ReadStringContent(reader, nfo, (musicVideo, album) => musicVideo.Album = album);
break;
case "plot":
await ReadStringContent(reader, nfo, (musicVideo, plot) => musicVideo.Plot = plot);
break;
case "year":
await ReadIntContent(reader, nfo, (musicVideo, year) => musicVideo.Year = year);
break;
case "genre":
await ReadStringContent(
reader,
nfo,
(musicVideo, genre) => musicVideo.Genres.Add(genre));
break;
case "tag":
await ReadStringContent(reader, nfo, (musicVideo, tag) => musicVideo.Tags.Add(tag));
break;
case "studio":
await ReadStringContent(
reader,
nfo,
(musicVideo, studio) => musicVideo.Studios.Add(studio));
break;
}
break;
case XmlNodeType.EndElement:
if (reader.Name == "musicvideo")
{
done = true;
}
break;
}
}
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
}
catch (Exception ex)
{
_client.Notify(ex);
return new FailedToReadNfo(ex.ToString());
}
}
}

2
ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs

@ -21,7 +21,7 @@ public class TvShowEpisodeNfo @@ -21,7 +21,7 @@ public class TvShowEpisodeNfo
public string ContentRating { get; set; }
[XmlElement("aired")]
public string Aired { get; set; }
public Option<DateTime> Aired { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }

2
ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs

@ -31,7 +31,7 @@ public class TvShowNfo @@ -31,7 +31,7 @@ public class TvShowNfo
public string ContentRating { get; set; }
[XmlElement("premiered")]
public string Premiered { get; set; }
public Option<DateTime> Premiered { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }

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

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

3
ErsatzTV/Startup.cs

@ -412,6 +412,9 @@ public class Startup @@ -412,6 +412,9 @@ public class Startup
services.AddScoped<IEmbySecretStore, EmbySecretStore>();
services.AddScoped<IEpisodeNfoReader, EpisodeNfoReader>();
services.AddScoped<IMovieNfoReader, MovieNfoReader>();
services.AddScoped<IArtistNfoReader, ArtistNfoReader>();
services.AddScoped<IMusicVideoNfoReader, MusicVideoNfoReader>();
services.AddScoped<ITvShowNfoReader, TvShowNfoReader>();
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));

Loading…
Cancel
Save