support multi-episode files in local libraries (#240)
* 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 cleanup
This commit is contained in:
@@ -50,7 +50,7 @@ namespace ErsatzTV.Application.MediaCards
|
||||
episodeMetadata.Episode.Season.ShowId,
|
||||
episodeMetadata.Episode.SeasonId,
|
||||
episodeMetadata.Episode.Season.SeasonNumber,
|
||||
episodeMetadata.Episode.EpisodeNumber,
|
||||
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(em => em.EpisodeNumber, () => 0),
|
||||
episodeMetadata.Title,
|
||||
episodeMetadata.SortTitle,
|
||||
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts
|
||||
@@ -31,9 +32,16 @@ namespace ErsatzTV.Application.Playouts
|
||||
case Episode e:
|
||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
||||
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
|
||||
return e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{showTitle}s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00} - {em.Title}")
|
||||
.IfNone("[unknown episode]");
|
||||
var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList();
|
||||
var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList();
|
||||
if (episodeNumbers.Count == 0 || episodeTitles.Count == 0)
|
||||
{
|
||||
return "[unknown episode]";
|
||||
}
|
||||
|
||||
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
|
||||
var titlesString = $"{string.Join('/', episodeTitles)}";
|
||||
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
|
||||
case Movie m:
|
||||
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
|
||||
case MusicVideo mv:
|
||||
|
||||
@@ -57,14 +57,18 @@ namespace ErsatzTV.Application.Television
|
||||
season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby))
|
||||
.IfNone(string.Empty));
|
||||
|
||||
internal static TelevisionEpisodeViewModel ProjectToViewModel(Episode episode) =>
|
||||
new(
|
||||
internal static TelevisionEpisodeViewModel ProjectToViewModel(Episode episode)
|
||||
{
|
||||
Option<EpisodeMetadata> maybeMetadata = episode.EpisodeMetadata.HeadOrNone();
|
||||
|
||||
return new TelevisionEpisodeViewModel(
|
||||
episode.Season.ShowId,
|
||||
episode.SeasonId,
|
||||
episode.EpisodeNumber,
|
||||
episode.EpisodeMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
|
||||
episode.EpisodeMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
episode.EpisodeMetadata.HeadOrNone().Map(m => GetThumbnail(m, None, None)).IfNone(string.Empty));
|
||||
maybeMetadata.Map(em => em.EpisodeNumber).IfNone(0),
|
||||
maybeMetadata.Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
|
||||
maybeMetadata.Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
maybeMetadata.Map(m => GetThumbnail(m, None, None)).IfNone(string.Empty));
|
||||
}
|
||||
|
||||
private static string GetPoster(
|
||||
Metadata metadata,
|
||||
|
||||
@@ -89,20 +89,13 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
public Task<List<int>> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber) => throw new NotSupportedException();
|
||||
public Task<Unit> RemoveMetadata(Episode episode, EpisodeMetadata metadata) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<bool> AddDirector(EpisodeMetadata metadata, Director director) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> AddWriter(EpisodeMetadata metadata, Writer writer) => throw new NotSupportedException();
|
||||
|
||||
public Task<int> GetShowCount() => throw new NotSupportedException();
|
||||
|
||||
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Show show) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Season season) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Episode episode) => throw new NotSupportedException();
|
||||
public Task<Unit> AddMetadata(Episode episode, EpisodeMetadata metadata) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using FluentAssertions;
|
||||
@@ -16,35 +15,33 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
private FallbackMetadataProvider _fallbackMetadataProvider;
|
||||
|
||||
[Test]
|
||||
[TestCase("Awesome Show - s01e02.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s01e02 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - s01e02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - S01E02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - s1e2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - S1E2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show - s01e02 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s01e02.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - s01e02 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - s01e02 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - S01E02 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - s1e2 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - S1E2 - Episode Title.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - s01e02 - Episode Title-720p.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02 - Episode Title-720p.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2 - Episode Title-720p.mkv", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2 - Episode Title-720p.mkv", 1, 2)]
|
||||
[TestCase(
|
||||
"Awesome Show (2021) - S01E02 - Description; More Description (1080p QUALITY codec GROUP).mkv",
|
||||
"Awesome Show (2021)",
|
||||
1,
|
||||
2)]
|
||||
[TestCase(
|
||||
"Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv",
|
||||
"Awesome.Show",
|
||||
1,
|
||||
2)]
|
||||
public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode)
|
||||
public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, int season, int episode)
|
||||
{
|
||||
(EpisodeMetadata metadata, int episodeNumber) = _fallbackMetadataProvider.GetFallbackMetadata(
|
||||
List<EpisodeMetadata> metadata = _fallbackMetadataProvider.GetFallbackMetadata(
|
||||
new Episode
|
||||
{
|
||||
LibraryPath = new LibraryPath(),
|
||||
@@ -60,10 +57,41 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
}
|
||||
});
|
||||
|
||||
metadata.Title.Should().Be(title);
|
||||
metadata.Count.Should().Be(1);
|
||||
// TODO: how can we test season number? do we need to?
|
||||
// metadata.Season.Should().Be(season);
|
||||
episodeNumber.Should().Be(episode);
|
||||
metadata.Head().EpisodeNumber.Should().Be(episode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("Awesome Show - s01e02-s01e03.mkv", 1, 2, 3)]
|
||||
[TestCase("Awesome Show - s01e02-whatever-s01e03-whatever2.mkv", 1, 2, 3)]
|
||||
[TestCase("Awesome Show - s01e02e03.mkv", 1, 2, 3)]
|
||||
[TestCase("Awesome Show - s01e02-03.mkv", 1, 2, 3)]
|
||||
public void GetFallbackMetadata_Should_Handle_Two_Episode_Formats(
|
||||
string path,
|
||||
int season,
|
||||
int episode1,
|
||||
int episode2)
|
||||
{
|
||||
List<EpisodeMetadata> metadata = _fallbackMetadataProvider.GetFallbackMetadata(
|
||||
new Episode
|
||||
{
|
||||
LibraryPath = new LibraryPath(),
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new()
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
new() { Path = path }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
metadata.Count.Should().Be(2);
|
||||
metadata.Map(m => m.EpisodeNumber).Should().BeEquivalentTo(episode1, episode2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,10 +302,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
DateTime? releaseDate = null) =>
|
||||
new()
|
||||
{
|
||||
EpisodeNumber = episode,
|
||||
EpisodeMetadata = new List<EpisodeMetadata>
|
||||
{
|
||||
new() { Title = title, ReleaseDate = releaseDate }
|
||||
new() { Title = title, ReleaseDate = releaseDate, EpisodeNumber = episode }
|
||||
},
|
||||
Season = new Season
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace ErsatzTV.Core.Domain
|
||||
[DebuggerDisplay("{EpisodeMetadata[0].Title}")]
|
||||
public class Episode : MediaItem
|
||||
{
|
||||
public int EpisodeNumber { get; set; }
|
||||
public int SeasonId { get; set; }
|
||||
public Season Season { get; set; }
|
||||
public List<EpisodeMetadata> EpisodeMetadata { get; set; }
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EpisodeMetadata : Metadata
|
||||
{
|
||||
public int EpisodeNumber { get; set; }
|
||||
public string Outline { get; set; }
|
||||
public string Plot { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
|
||||
@@ -335,7 +335,7 @@ namespace ErsatzTV.Core.Emby
|
||||
"UPDATE: Etag has changed for show {Show} season {Season} episode {Episode}",
|
||||
showName,
|
||||
seasonName,
|
||||
incoming.EpisodeNumber);
|
||||
incoming.EpisodeMetadata.HeadOrNone().Map(em => em.EpisodeNumber));
|
||||
|
||||
updateStatistics = true;
|
||||
incoming.SeasonId = season.Id;
|
||||
@@ -370,7 +370,7 @@ namespace ErsatzTV.Core.Emby
|
||||
"INSERT: Item id is new for show {Show} season {Season} episode {Episode}",
|
||||
showName,
|
||||
seasonName,
|
||||
incoming.EpisodeNumber);
|
||||
incoming.EpisodeMetadata.HeadOrNone().Map(em => em.EpisodeNumber));
|
||||
|
||||
if (await _televisionRepository.AddEpisode(incoming))
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
ShowMetadata GetFallbackMetadataForShow(string showFolder);
|
||||
ArtistMetadata GetFallbackMetadataForArtist(string artistFolder);
|
||||
Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode);
|
||||
List<EpisodeMetadata> GetFallbackMetadata(Episode episode);
|
||||
MovieMetadata GetFallbackMetadata(Movie movie);
|
||||
Option<MusicVideoMetadata> GetFallbackMetadata(MusicVideo musicVideo);
|
||||
string GetSortTitle(string title);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
|
||||
Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys);
|
||||
Task<List<int>> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys);
|
||||
Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber);
|
||||
Task<Unit> RemoveMetadata(Episode episode, EpisodeMetadata metadata);
|
||||
Task<bool> AddDirector(EpisodeMetadata metadata, Director director);
|
||||
Task<bool> AddWriter(EpisodeMetadata metadata, Writer writer);
|
||||
}
|
||||
|
||||
@@ -182,7 +182,8 @@ namespace ErsatzTV.Core.Iptv
|
||||
if (!isSameCustomShow)
|
||||
{
|
||||
int s = Optional(episode.Season?.SeasonNumber).IfNone(0);
|
||||
int e = episode.EpisodeNumber;
|
||||
// TODO: multi-episode?
|
||||
int e = episode.EpisodeMetadata.Head().EpisodeNumber;
|
||||
if (s > 0 && e > 0)
|
||||
{
|
||||
xml.WriteStartElement("episode-num");
|
||||
|
||||
@@ -336,7 +336,7 @@ namespace ErsatzTV.Core.Jellyfin
|
||||
"UPDATE: Etag has changed for show {Show} season {Season} episode {Episode}",
|
||||
showName,
|
||||
seasonName,
|
||||
incoming.EpisodeNumber);
|
||||
incoming.EpisodeMetadata.HeadOrNone().Map(em => em.EpisodeNumber));
|
||||
|
||||
updateStatistics = true;
|
||||
incoming.SeasonId = season.Id;
|
||||
@@ -371,7 +371,7 @@ namespace ErsatzTV.Core.Jellyfin
|
||||
"INSERT: Item id is new for show {Show} season {Season} episode {Episode}",
|
||||
showName,
|
||||
seasonName,
|
||||
incoming.EpisodeNumber);
|
||||
incoming.EpisodeMetadata.HeadOrNone().Map(em => em.EpisodeNumber));
|
||||
|
||||
if (await _televisionRepository.AddEpisode(incoming))
|
||||
{
|
||||
|
||||
@@ -26,13 +26,20 @@ namespace ErsatzTV.Core.Metadata
|
||||
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? artistFolder };
|
||||
}
|
||||
|
||||
public Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode)
|
||||
public List<EpisodeMetadata> GetFallbackMetadata(Episode episode)
|
||||
{
|
||||
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
string fileName = Path.GetFileName(path);
|
||||
var metadata = new EpisodeMetadata
|
||||
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, DateAdded = DateTime.UtcNow };
|
||||
return fileName != null ? GetEpisodeMetadata(fileName, metadata) : Tuple(metadata, 0);
|
||||
var baseMetadata = new EpisodeMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Fallback,
|
||||
Title = fileName ?? path,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
EpisodeNumber = 0
|
||||
};
|
||||
return fileName != null
|
||||
? GetEpisodeMetadata(fileName, baseMetadata)
|
||||
: new List<EpisodeMetadata> { baseMetadata };
|
||||
}
|
||||
|
||||
public MovieMetadata GetFallbackMetadata(Movie movie)
|
||||
@@ -87,18 +94,40 @@ namespace ErsatzTV.Core.Metadata
|
||||
return title;
|
||||
}
|
||||
|
||||
private Tuple<EpisodeMetadata, int> GetEpisodeMetadata(string fileName, EpisodeMetadata metadata)
|
||||
private static List<EpisodeMetadata> GetEpisodeMetadata(string fileName, EpisodeMetadata baseMetadata)
|
||||
{
|
||||
var result = new List<EpisodeMetadata>();
|
||||
|
||||
try
|
||||
{
|
||||
const string PATTERN = @"^(.*?)[.\s-]+[sS](\d+)[eE](\d+).*\.\w+$";
|
||||
Match match = Regex.Match(fileName, PATTERN);
|
||||
if (match.Success)
|
||||
const string PATTERN = @"[sS]\d+[eE]([e\-\d{1,2}]+)";
|
||||
MatchCollection matches = Regex.Matches(fileName, PATTERN);
|
||||
if (matches.Count > 0)
|
||||
{
|
||||
metadata.Title = match.Groups[1].Value;
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
metadata.Actors = new List<Actor>();
|
||||
return Tuple(metadata, int.Parse(match.Groups[3].Value));
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
string[] split = match.Groups[1].Value.Replace('e', '-').Split('-');
|
||||
foreach (string ep in split)
|
||||
{
|
||||
if (!int.TryParse(ep, out int episodeNumber))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadata = new EpisodeMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Fallback,
|
||||
EpisodeNumber = episodeNumber,
|
||||
DateAdded = baseMetadata.DateAdded,
|
||||
DateUpdated = baseMetadata.DateAdded,
|
||||
Actors = new List<Actor>()
|
||||
};
|
||||
|
||||
result.Add(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -106,7 +135,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
// ignored
|
||||
}
|
||||
|
||||
return Tuple(metadata, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private MovieMetadata GetMovieMetadata(string fileName, MovieMetadata metadata)
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
using LanguageExt;
|
||||
@@ -17,11 +18,11 @@ namespace ErsatzTV.Core.Metadata
|
||||
public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
{
|
||||
private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo));
|
||||
private static readonly XmlSerializer EpisodeSerializer = new(typeof(TvShowEpisodeNfo));
|
||||
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 IArtistRepository _artistRepository;
|
||||
private readonly IEpisodeNfoReader _episodeNfoReader;
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<LocalMetadataProvider> _logger;
|
||||
@@ -39,6 +40,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IEpisodeNfoReader episodeNfoReader,
|
||||
ILogger<LocalMetadataProvider> logger)
|
||||
{
|
||||
_metadataRepository = metadataRepository;
|
||||
@@ -48,6 +50,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_fallbackMetadataProvider = fallbackMetadataProvider;
|
||||
_localFileSystem = localFileSystem;
|
||||
_episodeNfoReader = episodeNfoReader;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -110,10 +113,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
() => Task.FromResult(false)));
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName) =>
|
||||
LoadEpisodeMetadata(episode, nfoFileName).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(episode, metadata),
|
||||
() => Task.FromResult(false)));
|
||||
LoadEpisodeMetadata(episode, nfoFileName).Bind(metadata => ApplyMetadataUpdate(episode, metadata));
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(Artist artist, string nfoFileName) =>
|
||||
LoadArtistMetadata(nfoFileName).Bind(
|
||||
@@ -174,116 +174,133 @@ namespace ErsatzTV.Core.Metadata
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ApplyMetadataUpdate(Episode episode, Tuple<EpisodeMetadata, int> metadataEpisodeNumber)
|
||||
private async Task<bool> ApplyMetadataUpdate(Episode episode, List<EpisodeMetadata> episodeMetadata)
|
||||
{
|
||||
(EpisodeMetadata metadata, int episodeNumber) = metadataEpisodeNumber;
|
||||
if (episode.EpisodeNumber != episodeNumber)
|
||||
episode.EpisodeMetadata ??= new List<EpisodeMetadata>();
|
||||
|
||||
var toUpdate = episode.EpisodeMetadata
|
||||
.Where(em => episodeMetadata.Any(em2 => em2.EpisodeNumber == em.EpisodeNumber))
|
||||
.ToList();
|
||||
var toRemove = episode.EpisodeMetadata.Except(toUpdate).ToList();
|
||||
var toAdd = episodeMetadata
|
||||
.Where(em => episode.EpisodeMetadata.All(em2 => em2.EpisodeNumber != em.EpisodeNumber))
|
||||
.ToList();
|
||||
|
||||
foreach (EpisodeMetadata metadata in toRemove)
|
||||
{
|
||||
await _televisionRepository.SetEpisodeNumber(episode, episodeNumber);
|
||||
await _televisionRepository.RemoveMetadata(episode, metadata);
|
||||
}
|
||||
|
||||
await Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
|
||||
async existing =>
|
||||
{
|
||||
existing.Outline = metadata.Outline;
|
||||
existing.Plot = metadata.Plot;
|
||||
existing.Tagline = metadata.Tagline;
|
||||
existing.Title = metadata.Title;
|
||||
foreach (EpisodeMetadata metadata in toAdd)
|
||||
{
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.EpisodeId = episode.Id;
|
||||
metadata.Episode = episode;
|
||||
episode.EpisodeMetadata.Add(metadata);
|
||||
|
||||
if (existing.DateAdded == DateTime.MinValue)
|
||||
await _metadataRepository.Add(metadata);
|
||||
}
|
||||
|
||||
foreach (EpisodeMetadata metadata in toUpdate)
|
||||
{
|
||||
Option<EpisodeMetadata> maybeExisting =
|
||||
episode.EpisodeMetadata.Find(em => em.EpisodeNumber == metadata.EpisodeNumber);
|
||||
await maybeExisting.Match(
|
||||
async existing =>
|
||||
{
|
||||
existing.DateAdded = metadata.DateAdded;
|
||||
}
|
||||
existing.Outline = metadata.Outline;
|
||||
existing.Plot = metadata.Plot;
|
||||
existing.Tagline = metadata.Tagline;
|
||||
existing.Title = metadata.Title;
|
||||
|
||||
existing.DateUpdated = metadata.DateUpdated;
|
||||
existing.MetadataKind = metadata.MetadataKind;
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.Year = metadata.Year;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
bool updated = await UpdateMetadataCollections(
|
||||
existing,
|
||||
metadata,
|
||||
(_, _) => Task.FromResult(false),
|
||||
(_, _) => Task.FromResult(false),
|
||||
(_, _) => Task.FromResult(false),
|
||||
_televisionRepository.AddActor);
|
||||
|
||||
foreach (Director director in existing.Directors
|
||||
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||
{
|
||||
existing.Directors.Remove(director);
|
||||
if (await _metadataRepository.RemoveDirector(director))
|
||||
if (existing.DateAdded == DateTime.MinValue)
|
||||
{
|
||||
updated = true;
|
||||
existing.DateAdded = metadata.DateAdded;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Director director in metadata.Directors
|
||||
.Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||
{
|
||||
existing.Directors.Add(director);
|
||||
if (await _televisionRepository.AddDirector(existing, director))
|
||||
existing.DateUpdated = metadata.DateUpdated;
|
||||
existing.MetadataKind = metadata.MetadataKind;
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.Year = metadata.Year;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
bool updated = await UpdateMetadataCollections(
|
||||
existing,
|
||||
metadata,
|
||||
(_, _) => Task.FromResult(false),
|
||||
(_, _) => Task.FromResult(false),
|
||||
(_, _) => Task.FromResult(false),
|
||||
_televisionRepository.AddActor);
|
||||
|
||||
foreach (Director director in existing.Directors
|
||||
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||
{
|
||||
updated = true;
|
||||
existing.Directors.Remove(director);
|
||||
if (await _metadataRepository.RemoveDirector(director))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Writer writer in existing.Writers
|
||||
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||
{
|
||||
existing.Writers.Remove(writer);
|
||||
if (await _metadataRepository.RemoveWriter(writer))
|
||||
foreach (Director director in metadata.Directors
|
||||
.Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||
{
|
||||
updated = true;
|
||||
existing.Directors.Add(director);
|
||||
if (await _televisionRepository.AddDirector(existing, director))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Writer writer in metadata.Writers
|
||||
.Filter(w => existing.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||
{
|
||||
existing.Writers.Add(writer);
|
||||
if (await _televisionRepository.AddWriter(existing, writer))
|
||||
foreach (Writer writer in existing.Writers
|
||||
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||
{
|
||||
updated = true;
|
||||
existing.Writers.Remove(writer);
|
||||
if (await _metadataRepository.RemoveWriter(writer))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (MetadataGuid guid in existing.Guids
|
||||
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||
{
|
||||
existing.Guids.Remove(guid);
|
||||
if (await _metadataRepository.RemoveGuid(guid))
|
||||
foreach (Writer writer in metadata.Writers
|
||||
.Filter(w => existing.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||
{
|
||||
updated = true;
|
||||
existing.Writers.Add(writer);
|
||||
if (await _televisionRepository.AddWriter(existing, writer))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (MetadataGuid guid in metadata.Guids
|
||||
.Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||
{
|
||||
existing.Guids.Add(guid);
|
||||
if (await _metadataRepository.AddGuid(existing, guid))
|
||||
foreach (MetadataGuid guid in existing.Guids
|
||||
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||
{
|
||||
updated = true;
|
||||
existing.Guids.Remove(guid);
|
||||
if (await _metadataRepository.RemoveGuid(guid))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await _metadataRepository.Update(existing) || updated;
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.EpisodeId = episode.Id;
|
||||
episode.EpisodeMetadata = new List<EpisodeMetadata> { metadata };
|
||||
foreach (MetadataGuid guid in metadata.Guids
|
||||
.Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||
{
|
||||
existing.Guids.Add(guid);
|
||||
if (await _metadataRepository.AddGuid(existing, guid))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return await _metadataRepository.Add(metadata);
|
||||
});
|
||||
return await _metadataRepository.Update(existing) || updated;
|
||||
},
|
||||
() => Task.FromResult(false));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -665,36 +682,45 @@ namespace ErsatzTV.Core.Metadata
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Option<Tuple<EpisodeMetadata, int>>> LoadEpisodeMetadata(Episode episode, string nfoFileName)
|
||||
private async Task<List<EpisodeMetadata>> LoadEpisodeMetadata(Episode episode, string nfoFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
|
||||
Option<TvShowEpisodeNfo> maybeNfo = EpisodeSerializer.Deserialize(fileStream) as TvShowEpisodeNfo;
|
||||
return maybeNfo.Match<Option<Tuple<EpisodeMetadata, int>>>(
|
||||
nfo =>
|
||||
{
|
||||
DateTime dateAdded = DateTime.UtcNow;
|
||||
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
|
||||
List<TvShowEpisodeNfo> nfos = await _episodeNfoReader.Read(fileStream);
|
||||
var result = new List<EpisodeMetadata>();
|
||||
foreach (TvShowEpisodeNfo nfo in nfos)
|
||||
{
|
||||
DateTime dateAdded = DateTime.UtcNow;
|
||||
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
|
||||
|
||||
var metadata = new EpisodeMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Sidecar,
|
||||
DateAdded = dateAdded,
|
||||
DateUpdated = dateUpdated,
|
||||
Title = nfo.Title,
|
||||
ReleaseDate = GetAired(0, nfo.Aired),
|
||||
Plot = nfo.Plot,
|
||||
Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
|
||||
Guids = nfo.UniqueIds
|
||||
.Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" })
|
||||
.ToList(),
|
||||
Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(),
|
||||
Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList()
|
||||
};
|
||||
return Tuple(metadata, nfo.Episode);
|
||||
},
|
||||
None);
|
||||
var metadata = new EpisodeMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Sidecar,
|
||||
DateAdded = dateAdded,
|
||||
DateUpdated = dateUpdated,
|
||||
Title = nfo.Title,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(nfo.Title),
|
||||
EpisodeNumber = nfo.Episode,
|
||||
Year = GetYear(0, nfo.Aired),
|
||||
ReleaseDate = GetAired(0, nfo.Aired),
|
||||
Plot = nfo.Plot,
|
||||
Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
|
||||
Guids = nfo.UniqueIds
|
||||
.Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" })
|
||||
.ToList(),
|
||||
Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(),
|
||||
Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(),
|
||||
Genres = new List<Genre>(),
|
||||
Tags = new List<Tag>(),
|
||||
Studios = new List<Studio>(),
|
||||
Artwork = new List<Artwork>()
|
||||
};
|
||||
|
||||
result.Add(metadata);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -883,7 +909,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
return updated;
|
||||
}
|
||||
|
||||
private List<Actor> Actors(List<ActorNfo> actorNfos, DateTime dateAdded, DateTime dateUpdated)
|
||||
private static List<Actor> Actors(List<ActorNfo> actorNfos, DateTime dateAdded, DateTime dateUpdated)
|
||||
{
|
||||
var result = new List<Actor>();
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -360,8 +360,10 @@ namespace ErsatzTV.Core.Metadata
|
||||
await LocateThumbnail(episode).IfSomeAsync(
|
||||
async posterFile =>
|
||||
{
|
||||
EpisodeMetadata metadata = episode.EpisodeMetadata.Head();
|
||||
await RefreshArtwork(posterFile, metadata, ArtworkKind.Thumbnail);
|
||||
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
|
||||
{
|
||||
await RefreshArtwork(posterFile, metadata, ArtworkKind.Thumbnail);
|
||||
}
|
||||
});
|
||||
|
||||
return episode;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
@@ -65,13 +66,13 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
int episode1 = x switch
|
||||
{
|
||||
Episode e => e.EpisodeNumber,
|
||||
Episode e => e.EpisodeMetadata.Max(em => em.EpisodeNumber),
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
int episode2 = y switch
|
||||
{
|
||||
Episode e => e.EpisodeNumber,
|
||||
Episode e => e.EpisodeMetadata.Max(em => em.EpisodeNumber),
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
|
||||
@@ -522,7 +522,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
||||
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
|
||||
return e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{showTitle}s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00} - {em.Title}")
|
||||
.Map(em => $"{showTitle}s{e.Season.SeasonNumber:00}e{em.EpisodeNumber:00} - {em.Title}")
|
||||
.IfNone("[unknown episode]");
|
||||
case Movie m:
|
||||
return m.MovieMetadata.HeadOrNone().Match(mm => mm.Title ?? string.Empty, () => "[unknown movie]");
|
||||
|
||||
@@ -423,9 +423,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
episode.Id = existing.Id;
|
||||
|
||||
existing.Etag = episode.Etag;
|
||||
existing.EpisodeNumber = episode.EpisodeNumber;
|
||||
|
||||
// metadata
|
||||
// TODO: multiple metadata?
|
||||
EpisodeMetadata metadata = existing.EpisodeMetadata.Head();
|
||||
EpisodeMetadata incomingMetadata = episode.EpisodeMetadata.Head();
|
||||
metadata.Title = incomingMetadata.Title;
|
||||
@@ -435,6 +435,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
metadata.DateAdded = incomingMetadata.DateAdded;
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
||||
metadata.EpisodeNumber = incomingMetadata.EpisodeNumber;
|
||||
|
||||
// thumbnail
|
||||
Artwork incomingThumbnail =
|
||||
|
||||
@@ -423,9 +423,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
episode.Id = existing.Id;
|
||||
|
||||
existing.Etag = episode.Etag;
|
||||
existing.EpisodeNumber = episode.EpisodeNumber;
|
||||
|
||||
// metadata
|
||||
// TODO: multiple metadata?
|
||||
EpisodeMetadata metadata = existing.EpisodeMetadata.Head();
|
||||
EpisodeMetadata incomingMetadata = episode.EpisodeMetadata.Head();
|
||||
metadata.Title = incomingMetadata.Title;
|
||||
@@ -435,6 +435,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
metadata.DateAdded = incomingMetadata.DateAdded;
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
||||
metadata.EpisodeNumber = metadata.EpisodeNumber;
|
||||
|
||||
// thumbnail
|
||||
Artwork incomingThumbnail =
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.ThenInclude(sm => sm.Actors)
|
||||
.ThenInclude(a => a.Artwork)
|
||||
.OrderBy(em => em.Episode.EpisodeNumber)
|
||||
.OrderBy(em => em.EpisodeNumber)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
@@ -521,12 +521,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
return ids;
|
||||
}
|
||||
|
||||
public async Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber)
|
||||
public async Task<Unit> RemoveMetadata(Episode episode, EpisodeMetadata metadata)
|
||||
{
|
||||
episode.EpisodeNumber = episodeNumber;
|
||||
episode.EpisodeMetadata.Remove(metadata);
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE Episode SET EpisodeNumber = @EpisodeNumber WHERE Id = @Id",
|
||||
new { EpisodeNumber = episodeNumber, episode.Id });
|
||||
@"DELETE FROM EpisodeMetadata WHERE Id = @MetadataId",
|
||||
new { MetadataId = metadata.Id });
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
|
||||
@@ -512,11 +512,6 @@ namespace ErsatzTV.Infrastructure.Emby
|
||||
EpisodeMetadata = new List<EpisodeMetadata> { metadata }
|
||||
};
|
||||
|
||||
if (item.IndexNumber.HasValue)
|
||||
{
|
||||
episode.EpisodeNumber = item.IndexNumber.Value;
|
||||
}
|
||||
|
||||
return episode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -549,6 +544,11 @@ namespace ErsatzTV.Infrastructure.Emby
|
||||
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
|
||||
};
|
||||
|
||||
if (item.IndexNumber.HasValue)
|
||||
{
|
||||
metadata.EpisodeNumber = item.IndexNumber.Value;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
|
||||
{
|
||||
metadata.ReleaseDate = releaseDate;
|
||||
|
||||
@@ -568,11 +568,6 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
||||
EpisodeMetadata = new List<EpisodeMetadata> { metadata }
|
||||
};
|
||||
|
||||
if (item.IndexNumber.HasValue)
|
||||
{
|
||||
episode.EpisodeNumber = item.IndexNumber.Value;
|
||||
}
|
||||
|
||||
return episode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -605,6 +600,11 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
||||
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
|
||||
};
|
||||
|
||||
if (item.IndexNumber.HasValue)
|
||||
{
|
||||
metadata.EpisodeNumber = item.IndexNumber.Value;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
|
||||
{
|
||||
metadata.ReleaseDate = releaseDate;
|
||||
|
||||
Generated
+2892
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2892
File diff suppressed because it is too large
Load Diff
+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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+2889
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,6 +348,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int>("EpisodeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EpisodeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MetadataKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1421,9 +1424,6 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
|
||||
|
||||
b.Property<int>("EpisodeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -655,7 +655,6 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
var episode = new PlexEpisode
|
||||
{
|
||||
Key = response.Key,
|
||||
EpisodeNumber = response.Index,
|
||||
EpisodeMetadata = new List<EpisodeMetadata> { metadata },
|
||||
MediaVersions = new List<MediaVersion> { version }
|
||||
};
|
||||
@@ -673,6 +672,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = response.Title,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
|
||||
EpisodeNumber = response.Index,
|
||||
Plot = response.Summary,
|
||||
Year = response.Year,
|
||||
Tagline = response.Tagline,
|
||||
|
||||
@@ -593,17 +593,15 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
|
||||
try
|
||||
{
|
||||
var doc = new Document
|
||||
{
|
||||
new StringField(IdField, episode.Id.ToString(), Field.Store.YES),
|
||||
new StringField(TypeField, EpisodeType, Field.Store.NO),
|
||||
new TextField(TitleField, metadata.Title, Field.Store.NO),
|
||||
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
|
||||
new TextField(LibraryNameField, episode.LibraryPath.Library.Name, Field.Store.NO),
|
||||
new StringField(LibraryIdField, episode.LibraryPath.Library.Id.ToString(), Field.Store.NO),
|
||||
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
|
||||
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
|
||||
};
|
||||
var doc = new Document();
|
||||
doc.Add(new StringField(IdField, episode.Id.ToString(), Field.Store.YES));
|
||||
doc.Add(new StringField(TypeField, EpisodeType, Field.Store.NO));
|
||||
doc.Add(new TextField(TitleField, metadata.Title, Field.Store.NO));
|
||||
doc.Add(new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO));
|
||||
doc.Add(new TextField(LibraryNameField, episode.LibraryPath.Library.Name, Field.Store.NO));
|
||||
doc.Add(new StringField(LibraryIdField, episode.LibraryPath.Library.Id.ToString(), Field.Store.NO));
|
||||
doc.Add(new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO));
|
||||
doc.Add(new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES));
|
||||
|
||||
AddLanguages(doc, episode.MediaVersions);
|
||||
|
||||
@@ -682,7 +680,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
metadata switch
|
||||
{
|
||||
EpisodeMetadata em =>
|
||||
$"{em.Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.Episode.EpisodeNumber}"
|
||||
$"{em.Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.EpisodeNumber}"
|
||||
.ToLowerInvariant(),
|
||||
_ => $"{metadata.Title}_{metadata.Year}".ToLowerInvariant()
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=discardcorrupt/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=drawtext/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Emby/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=episodedetails/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ersatztv/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=etvignore/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=fanart/@EntryIndexedValue">True</s:Boolean>
|
||||
@@ -32,6 +33,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=maxrate/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=movflags/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mpaa/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mpegts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=muxdelay/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=muxpreload/@EntryIndexedValue">True</s:Boolean>
|
||||
@@ -41,7 +43,10 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Playouts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=probesize/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=showtitle/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tvdb/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tvshow/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=uniqueid/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vaapi/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=xmltv/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=yadif/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -17,6 +17,7 @@ using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
@@ -24,6 +25,7 @@ using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Formatters;
|
||||
@@ -252,6 +254,7 @@ namespace ErsatzTV
|
||||
});
|
||||
services.AddScoped<IJellyfinSecretStore, JellyfinSecretStore>();
|
||||
services.AddScoped<IEmbySecretStore, EmbySecretStore>();
|
||||
services.AddScoped<IEpisodeNfoReader, EpisodeNfoReader>();
|
||||
|
||||
services.AddHostedService<EndpointValidatorService>();
|
||||
services.AddHostedService<DatabaseMigratorService>();
|
||||
|
||||
Reference in New Issue
Block a user