mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* add unused episode nfo reader * move episode number from episode to episode metadata * first pass at loading multi-episode metadata from nfo files * fix episode scanning * local multi-part episode fixes * code cleanuppull/241/head
37 changed files with 9510 additions and 221 deletions
@ -0,0 +1,239 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Text; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Metadata.Nfo; |
||||||
|
using FluentAssertions; |
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Tests.Metadata.Nfo |
||||||
|
{ |
||||||
|
[TestFixture] |
||||||
|
public class EpisodeNfoReaderTests |
||||||
|
{ |
||||||
|
[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.Read(stream); |
||||||
|
|
||||||
|
result.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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<showtitle>show</showtitle> |
||||||
|
<title>episode-one</title> |
||||||
|
<episode>1</episode> |
||||||
|
<season>1</season> |
||||||
|
</episodedetails> |
||||||
|
<episodedetails> |
||||||
|
<showtitle>show</showtitle> |
||||||
|
<title>episode-two</title> |
||||||
|
<episode>2</episode> |
||||||
|
<season>1</season> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.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); |
||||||
|
} |
||||||
|
|
||||||
|
[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<uniqueid default=""true"" type=""tvdb"">12345</uniqueid> |
||||||
|
<uniqueid default=""false"" type=""imdb"">tt54321</uniqueid> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.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); |
||||||
|
} |
||||||
|
|
||||||
|
[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<mpaa/> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.Read(stream); |
||||||
|
|
||||||
|
result.Count.Should().Be(1); |
||||||
|
result[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<mpaa>US:Something</mpaa> |
||||||
|
</episodedetails> |
||||||
|
<episodedetails> |
||||||
|
<mpaa>US:Something / US:SomethingElse</mpaa> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.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); |
||||||
|
} |
||||||
|
|
||||||
|
[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<plot/> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.Read(stream); |
||||||
|
|
||||||
|
result.Count.Should().Be(1); |
||||||
|
result[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<plot>Some Plot</plot> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.Read(stream); |
||||||
|
|
||||||
|
result.Count.Should().Be(1); |
||||||
|
result[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<actor> |
||||||
|
<name>Name 1</name> |
||||||
|
<role>Role 1</role> |
||||||
|
<thumb>Thumb 1</thumb> |
||||||
|
</actor> |
||||||
|
<actor> |
||||||
|
<name>Name 2</name> |
||||||
|
<role>Role 2</role> |
||||||
|
<thumb>Thumb 2</thumb> |
||||||
|
</actor> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.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); |
||||||
|
} |
||||||
|
|
||||||
|
[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<credits>Writer 1</credits> |
||||||
|
</episodedetails> |
||||||
|
<episodedetails> |
||||||
|
<credits>Writer 2</credits> |
||||||
|
<credits>Writer 3</credits> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.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); |
||||||
|
} |
||||||
|
|
||||||
|
[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""?>
|
||||||
|
<!--created on whatever - comment--> |
||||||
|
<episodedetails> |
||||||
|
<director>Director 1</director> |
||||||
|
</episodedetails> |
||||||
|
<episodedetails> |
||||||
|
<director>Director 2</director> |
||||||
|
<director>Director 3</director> |
||||||
|
</episodedetails>"));
|
||||||
|
|
||||||
|
List<TvShowEpisodeNfo> result = await reader.Read(stream); |
||||||
|
|
||||||
|
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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Metadata.Nfo; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo |
||||||
|
{ |
||||||
|
public interface IEpisodeNfoReader |
||||||
|
{ |
||||||
|
Task<List<TvShowEpisodeNfo>> Read(Stream input); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,201 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using System.Xml; |
||||||
|
using System.Xml.Linq; |
||||||
|
using ErsatzTV.Core.Interfaces.Metadata.Nfo; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Metadata.Nfo |
||||||
|
{ |
||||||
|
public class EpisodeNfoReader : IEpisodeNfoReader |
||||||
|
{ |
||||||
|
public async Task<List<TvShowEpisodeNfo>> Read(Stream input) |
||||||
|
{ |
||||||
|
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()) |
||||||
|
{ |
||||||
|
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 ReadTitle(reader, nfo); |
||||||
|
break; |
||||||
|
case "showtitle": |
||||||
|
await ReadShowTitle(reader, nfo); |
||||||
|
break; |
||||||
|
case "episode": |
||||||
|
await ReadEpisode(reader, nfo); |
||||||
|
break; |
||||||
|
case "season": |
||||||
|
await ReadSeason(reader, nfo); |
||||||
|
break; |
||||||
|
case "uniqueid": |
||||||
|
await ReadUniqueId(reader, nfo); |
||||||
|
break; |
||||||
|
case "mpaa": |
||||||
|
await ReadContentRating(reader, nfo); |
||||||
|
break; |
||||||
|
case "aired": |
||||||
|
// TODO: parse the date here
|
||||||
|
await ReadAired(reader, nfo); |
||||||
|
break; |
||||||
|
case "plot": |
||||||
|
await ReadPlot(reader, nfo); |
||||||
|
break; |
||||||
|
case "actor": |
||||||
|
ReadActor(reader, nfo); |
||||||
|
break; |
||||||
|
case "credits": |
||||||
|
await ReadWriter(reader, nfo); |
||||||
|
break; |
||||||
|
case "director": |
||||||
|
await ReadDirector(reader, nfo); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
case XmlNodeType.EndElement: |
||||||
|
switch (reader.Name.ToLowerInvariant()) |
||||||
|
{ |
||||||
|
case "episodedetails": |
||||||
|
if (nfo != null) |
||||||
|
{ |
||||||
|
result.Add(nfo); |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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) |
||||||
|
{ |
||||||
|
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()); |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations |
||||||
|
{ |
||||||
|
public partial class Add_EpisodeMetadataEpisodeNumber : Migration |
||||||
|
{ |
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "EpisodeNumber", |
||||||
|
table: "EpisodeMetadata", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "EpisodeNumber", |
||||||
|
table: "EpisodeMetadata"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,17 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations |
||||||
|
{ |
||||||
|
public partial class Update_EpisodeMetadataEpisodeNumber : Migration |
||||||
|
{ |
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.Sql( |
||||||
|
@"UPDATE EpisodeMetadata SET EpisodeNumber = (SELECT EpisodeNumber FROM Episode WHERE Id = EpisodeMetadata.EpisodeId)"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations |
||||||
|
{ |
||||||
|
public partial class Remove_EpisodeEpisodeNumber : Migration |
||||||
|
{ |
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "EpisodeNumber", |
||||||
|
table: "Episode"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "EpisodeNumber", |
||||||
|
table: "Episode", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue