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:
Jason Dove
2021-06-04 06:00:35 -05:00
committed by GitHub
parent ce9ef72799
commit 3e3bbcf38e
37 changed files with 9511 additions and 222 deletions
+1 -1
View File
@@ -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(
+11 -3
View File
@@ -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:
+10 -6
View File
@@ -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);
}
+2 -1
View File
@@ -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)
+141 -115
View File
@@ -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
};
+1 -1
View File
@@ -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;
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");
}
}
}
@@ -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);
}
}
}
@@ -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,
+10 -12
View File
@@ -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()
};
+5
View File
@@ -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>
+3
View File
@@ -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>();