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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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