support movie nfo metadata in other video libraries (#788)
* add other video nfo metadata * update docs
This commit is contained in:
@@ -13,10 +13,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Fix occasional erroneous log messages when HLS channel playback times out because all clients have left
|
||||
- Fix fallback filler playback
|
||||
- Fix stream continuity when error messages are displayed
|
||||
- Fix duplicate scanning within other video libraries (i.e. folders would be scanned multiple times)
|
||||
|
||||
### Added
|
||||
- Add `show_genre` and `show_tag` to search index for seasons and episodes
|
||||
- Use `aired` value to source release date from music video nfo metadata
|
||||
- Add NFO metadata support to `Other Video` libraries
|
||||
- `Other Video` NFO metadata must be in the movie NFO metadata format
|
||||
|
||||
## [0.5.5-beta] - 2022-05-03
|
||||
### Fixed
|
||||
|
||||
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using Humanizer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources;
|
||||
@@ -147,7 +148,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
|
||||
_logger.LogDebug(
|
||||
"Scan of library {Name} completed in {Duration}",
|
||||
localLibrary.Name,
|
||||
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
|
||||
sw.Elapsed.Humanize());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
|
||||
|
||||
[TestFixture]
|
||||
public class OtherVideoNfoReaderTests
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp() => _otherVideoNfoReader = new OtherVideoNfoReader(new Mock<IClient>().Object);
|
||||
|
||||
private OtherVideoNfoReader _otherVideoNfoReader;
|
||||
|
||||
[Test]
|
||||
public async Task ParsingNfo_Should_Return_Error()
|
||||
{
|
||||
await using var stream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars"));
|
||||
|
||||
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
|
||||
|
||||
result.IsLeft.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task MetadataNfo_Should_Return_Nfo()
|
||||
{
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie></movie>"));
|
||||
|
||||
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CombinationNfo_Should_Return_Nfo()
|
||||
{
|
||||
await using var stream = new MemoryStream(
|
||||
Encoding.UTF8.GetBytes(
|
||||
@"<movie></movie>
|
||||
https://www.themoviedb.org/movie/11-star-wars"));
|
||||
|
||||
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task FullSample_Should_Return_Nfo()
|
||||
{
|
||||
await using var stream = new MemoryStream(
|
||||
Encoding.UTF8.GetBytes(
|
||||
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
|
||||
<movie>
|
||||
<title>Zack Snyder's Justice League</title>
|
||||
<originaltitle>Zack Snyder's Justice League</originaltitle>
|
||||
<sorttitle>Justice League 2</sorttitle>
|
||||
<ratings>
|
||||
<rating name=""imdb"" max=""10"" default=""true"">
|
||||
<value>8.300000</value>
|
||||
<votes>197786</votes>
|
||||
</rating>
|
||||
<rating name=""themoviedb"" max=""10"">
|
||||
<value>8.700000</value>
|
||||
<votes>3461</votes>
|
||||
</rating>
|
||||
<rating name=""trakt"" max=""10"">
|
||||
<value>8.195670</value>
|
||||
<votes>4247</votes>
|
||||
</rating>
|
||||
</ratings>
|
||||
<userrating>0</userrating>
|
||||
<top250>140</top250>
|
||||
<outline></outline>
|
||||
<plot>Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.</plot>
|
||||
<tagline></tagline>
|
||||
<runtime>242</runtime>
|
||||
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdb873f474.jpg</thumb>
|
||||
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://image.tmdb.org/t/p/original/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg</thumb>
|
||||
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviethumb/zack-snyders-justice-league-6050310135cf6.jpg</thumb>
|
||||
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://image.tmdb.org/t/p/original/wcYBuOZDP6Vi8Ye4qax3Zx9dCan.jpg</thumb>
|
||||
<thumb spoof="""" cache="""" aspect=""keyart"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdba9bdd16.jpg</thumb>
|
||||
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview="""">https://assets.fanart.tv/fanart/movies/791373/hdmovielogo/zack-snyders-justice-league-5ed3f2e4952e9.png</thumb>
|
||||
<thumb spoof="""" cache="""" aspect=""banner"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviebanner/zack-snyders-justice-league-6050049514d4c.jpg</thumb>
|
||||
<fanart>
|
||||
<thumb colors="""" preview=""https://assets.fanart.tv/preview/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg"">https://assets.fanart.tv/fanart/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg</thumb>
|
||||
<thumb colors="""" preview=""https://image.tmdb.org/t/p/w780/43NwryODVEsbBDC0jK3wYfVyb5q.jpg"">https://image.tmdb.org/t/p/original/43NwryODVEsbBDC0jK3wYfVyb5q.jpg</thumb>
|
||||
</fanart>
|
||||
<mpaa>Australia:M</mpaa>
|
||||
<playcount>0</playcount>
|
||||
<lastplayed></lastplayed>
|
||||
<id>791373</id>
|
||||
<uniqueid type=""imdb"">tt12361974</uniqueid>
|
||||
<uniqueid type=""tmdb"" default=""true"">791373</uniqueid>
|
||||
<genre>SuperHero</genre>
|
||||
<tag>TV Recording</tag>
|
||||
<set>
|
||||
<name>Justice League Collection</name>
|
||||
<overview>Based on the DC Comics superhero team</overview>
|
||||
</set>
|
||||
<country>USA</country>
|
||||
<credits>Chris Terrio</credits>
|
||||
<director>Zack Snyder</director>
|
||||
<premiered>2021-03-18</premiered>
|
||||
<year>2021</year>
|
||||
<status></status>
|
||||
<code></code>
|
||||
<aired></aired>
|
||||
<studio>Warner Bros. Pictures</studio>
|
||||
<trailer></trailer>
|
||||
<fileinfo>
|
||||
<streamdetails>
|
||||
<video>
|
||||
<codec>hevc</codec>
|
||||
<aspect>1.777778</aspect>
|
||||
<width>1920</width>
|
||||
<height>1080</height>
|
||||
<durationinseconds>14528</durationinseconds>
|
||||
<stereomode></stereomode>
|
||||
</video>
|
||||
<audio>
|
||||
<codec>ac3</codec>
|
||||
<language>eng</language>
|
||||
<channels>6</channels>
|
||||
</audio>
|
||||
<audio>
|
||||
<codec>ac3</codec>
|
||||
<language>fre</language>
|
||||
<channels>6</channels>
|
||||
</audio>
|
||||
<subtitle>
|
||||
<language>eng</language>
|
||||
</subtitle>
|
||||
</streamdetails>
|
||||
</fileinfo>
|
||||
<actor>
|
||||
<name>Ben Affleck</name>
|
||||
<role>Bruce Wayne / Batman</role>
|
||||
<order>0</order>
|
||||
<thumb>https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg</thumb>
|
||||
</actor>
|
||||
<actor>
|
||||
<name>Henry Cavill</name>
|
||||
<role>Clark Kent / Superman / Kal-El</role>
|
||||
<order>1</order>
|
||||
<thumb>https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg</thumb>
|
||||
</actor>
|
||||
<actor>
|
||||
<name>Gal Gadot</name>
|
||||
<role>Diana Prince / Wonder Woman</role>
|
||||
<order>2</order>
|
||||
<thumb>https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg</thumb>
|
||||
</actor>
|
||||
<resume>
|
||||
<position>0.000000</position>
|
||||
<total>0.000000</total>
|
||||
</resume>
|
||||
<dateadded>2021-03-26 11:35:50</dateadded>
|
||||
</movie>"));
|
||||
|
||||
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
foreach (OtherVideoNfo nfo in result.RightToSeq())
|
||||
{
|
||||
nfo.Title.Should().Be("Zack Snyder's Justice League");
|
||||
nfo.SortTitle.Should().Be("Justice League 2");
|
||||
nfo.Outline.Should().BeNullOrEmpty();
|
||||
nfo.Year.Should().Be(2021);
|
||||
nfo.ContentRating.Should().Be("Australia:M");
|
||||
|
||||
nfo.Premiered.IsSome.Should().BeTrue();
|
||||
foreach (DateTime premiered in nfo.Premiered)
|
||||
{
|
||||
premiered.Should().Be(new DateTime(2021, 03, 18));
|
||||
}
|
||||
|
||||
nfo.Plot.Should().Be(
|
||||
"Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.");
|
||||
nfo.Tagline.Should().BeNullOrEmpty();
|
||||
nfo.Genres.Should().BeEquivalentTo(new List<string> { "SuperHero" });
|
||||
nfo.Tags.Should().BeEquivalentTo(new List<string> { "TV Recording" });
|
||||
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Warner Bros. Pictures" });
|
||||
nfo.Actors.Should().BeEquivalentTo(
|
||||
new List<ActorNfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Ben Affleck", Order = 0, Role = "Bruce Wayne / Batman",
|
||||
Thumb = "https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Henry Cavill", Order = 1, Role = "Clark Kent / Superman / Kal-El",
|
||||
Thumb = "https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Gal Gadot", Order = 2, Role = "Diana Prince / Wonder Woman",
|
||||
Thumb = "https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg"
|
||||
}
|
||||
});
|
||||
nfo.Writers.Should().BeEquivalentTo(new List<string> { "Chris Terrio" });
|
||||
nfo.Directors.Should().BeEquivalentTo(new List<string> { "Zack Snyder" });
|
||||
nfo.UniqueIds.Should().BeEquivalentTo(
|
||||
new List<UniqueIdNfo>
|
||||
{
|
||||
new() { Type = "imdb", Guid = "tt12361974", Default = false },
|
||||
new() { Type = "tmdb", Guid = "791373", Default = true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task MetadataNfo_With_Tag_Should_Return_Nfo()
|
||||
{
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><tag>Test Tag</tag></movie>"));
|
||||
|
||||
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
foreach (OtherVideoNfo nfo in result.RightToSeq())
|
||||
{
|
||||
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task MetadataNfo_With_Outline_Should_Return_Nfo()
|
||||
{
|
||||
await using var stream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><outline>Test Outline</outline></movie>"));
|
||||
|
||||
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
foreach (OtherVideoNfo nfo in result.RightToSeq())
|
||||
{
|
||||
nfo.Outline.Should().Be("Test Outline");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
public class OtherVideoMetadata : Metadata
|
||||
{
|
||||
public string ContentRating { get; set; }
|
||||
public string Outline { get; set; }
|
||||
public string Plot { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public int OtherVideoId { get; set; }
|
||||
public OtherVideo OtherVideo { get; set; }
|
||||
public List<Director> Directors { get; set; }
|
||||
public List<Writer> Writers { get; set; }
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public interface ILocalMetadataProvider
|
||||
Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(Artist artist, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(OtherVideo otherVideo, string nfoFileName);
|
||||
Task<bool> RefreshTagMetadata(Song song, string ffprobePath);
|
||||
Task<bool> RefreshFallbackMetadata(Movie movie);
|
||||
Task<bool> RefreshFallbackMetadata(Episode episode);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
|
||||
|
||||
public interface IOtherVideoNfoReader
|
||||
{
|
||||
Task<Either<BaseError, OtherVideoNfo>> Read(Stream input);
|
||||
}
|
||||
@@ -8,9 +8,12 @@ public interface IOtherVideoRepository
|
||||
Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> GetOrAdd(LibraryPath libraryPath, string path);
|
||||
Task<IEnumerable<string>> FindOtherVideoPaths(LibraryPath libraryPath);
|
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
Task<bool> AddGenre(OtherVideoMetadata metadata, Genre genre);
|
||||
Task<bool> AddTag(OtherVideoMetadata metadata, Tag tag);
|
||||
Task<bool> AddStudio(OtherVideoMetadata metadata, Studio studio);
|
||||
Task<bool> AddActor(OtherVideoMetadata metadata, Actor actor);
|
||||
Task<bool> AddDirector(OtherVideoMetadata metadata, Director director);
|
||||
Task<bool> AddWriter(OtherVideoMetadata metadata, Writer writer);
|
||||
|
||||
Task<List<OtherVideoMetadata>> GetOtherVideosForCards(List<int> ids);
|
||||
// Task<int> GetOtherVideoCount(int artistId);
|
||||
// Task<List<OtherVideoMetadata>> GetPagedOtherVideos(int artistId, int pageNumber, int pageSize);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
private readonly IMusicVideoNfoReader _musicVideoNfoReader;
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
private readonly IOtherVideoNfoReader _otherVideoNfoReader;
|
||||
private readonly IOtherVideoRepository _otherVideoRepository;
|
||||
private readonly ISongRepository _songRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
@@ -44,6 +45,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
IArtistNfoReader artistNfoReader,
|
||||
IMusicVideoNfoReader musicVideoNfoReader,
|
||||
ITvShowNfoReader tvShowNfoReader,
|
||||
IOtherVideoNfoReader otherVideoNfoReader,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
IClient client,
|
||||
ILogger<LocalMetadataProvider> logger)
|
||||
@@ -62,6 +64,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
_artistNfoReader = artistNfoReader;
|
||||
_musicVideoNfoReader = musicVideoNfoReader;
|
||||
_tvShowNfoReader = tvShowNfoReader;
|
||||
_otherVideoNfoReader = otherVideoNfoReader;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
@@ -107,38 +110,77 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
return fallbackMetadata;
|
||||
}
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName) =>
|
||||
LoadMovieMetadata(movie, nfoFileName).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(movie, metadata),
|
||||
() => Task.FromResult(false)));
|
||||
public async Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName)
|
||||
{
|
||||
Option<MovieMetadata> maybeMetadata = await LoadMovieMetadata(movie, nfoFileName);
|
||||
foreach (MovieMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(movie, metadata);
|
||||
}
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName) =>
|
||||
LoadTelevisionShowMetadata(nfoFileName).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(televisionShow, metadata),
|
||||
() => Task.FromResult(false)));
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName) =>
|
||||
LoadEpisodeMetadata(episode, nfoFileName).Bind(metadata => ApplyMetadataUpdate(episode, metadata));
|
||||
public async Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName)
|
||||
{
|
||||
Option<ShowMetadata> maybeMetadata = await LoadTelevisionShowMetadata(nfoFileName);
|
||||
foreach (ShowMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(televisionShow, metadata);
|
||||
}
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(Artist artist, string nfoFileName) =>
|
||||
LoadArtistMetadata(nfoFileName).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(artist, metadata),
|
||||
() => Task.FromResult(false)));
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName) =>
|
||||
LoadMusicVideoMetadata(nfoFileName).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(musicVideo, metadata),
|
||||
() => RefreshFallbackMetadata(musicVideo)));
|
||||
public async Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName)
|
||||
{
|
||||
List<EpisodeMetadata> metadata = await LoadEpisodeMetadata(episode, nfoFileName);
|
||||
return await ApplyMetadataUpdate(episode, metadata);
|
||||
}
|
||||
|
||||
public Task<bool> RefreshTagMetadata(Song song, string ffprobePath) =>
|
||||
LoadSongMetadata(song, ffprobePath).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(song, metadata),
|
||||
() => RefreshFallbackMetadata(song)));
|
||||
public async Task<bool> RefreshSidecarMetadata(Artist artist, string nfoFileName)
|
||||
{
|
||||
Option<ArtistMetadata> maybeMetadata = await LoadArtistMetadata(nfoFileName);
|
||||
foreach (ArtistMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(artist, metadata);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName)
|
||||
{
|
||||
Option<MusicVideoMetadata> maybeMetadata = await LoadMusicVideoMetadata(nfoFileName);
|
||||
foreach (MusicVideoMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(musicVideo, metadata);
|
||||
}
|
||||
|
||||
return await RefreshFallbackMetadata(musicVideo);
|
||||
}
|
||||
|
||||
public async Task<bool> RefreshSidecarMetadata(OtherVideo otherVideo, string nfoFileName)
|
||||
{
|
||||
Option<OtherVideoMetadata> maybeMetadata = await LoadOtherVideoMetadata(nfoFileName);
|
||||
foreach (OtherVideoMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(otherVideo, metadata);
|
||||
}
|
||||
|
||||
return await RefreshFallbackMetadata(otherVideo);
|
||||
}
|
||||
|
||||
public async Task<bool> RefreshTagMetadata(Song song, string ffprobePath)
|
||||
{
|
||||
Option<SongMetadata> maybeMetadata = await LoadSongMetadata(song, ffprobePath);
|
||||
foreach (SongMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(song, metadata);
|
||||
}
|
||||
|
||||
return await RefreshFallbackMetadata(song);
|
||||
}
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(Movie movie) =>
|
||||
ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie));
|
||||
@@ -149,20 +191,38 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
public Task<bool> RefreshFallbackMetadata(Artist artist, string artistFolder) =>
|
||||
ApplyMetadataUpdate(artist, _fallbackMetadataProvider.GetFallbackMetadataForArtist(artistFolder));
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(OtherVideo otherVideo) =>
|
||||
_fallbackMetadataProvider.GetFallbackMetadata(otherVideo).Match(
|
||||
metadata => ApplyMetadataUpdate(otherVideo, metadata),
|
||||
() => Task.FromResult(false));
|
||||
public async Task<bool> RefreshFallbackMetadata(OtherVideo otherVideo)
|
||||
{
|
||||
Option<OtherVideoMetadata> maybeMetadata = _fallbackMetadataProvider.GetFallbackMetadata(otherVideo);
|
||||
foreach (OtherVideoMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(otherVideo, metadata);
|
||||
}
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(Song song) =>
|
||||
_fallbackMetadataProvider.GetFallbackMetadata(song).Match(
|
||||
metadata => ApplyMetadataUpdate(song, metadata),
|
||||
() => Task.FromResult(false));
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo) =>
|
||||
_fallbackMetadataProvider.GetFallbackMetadata(musicVideo).Match(
|
||||
metadata => ApplyMetadataUpdate(musicVideo, metadata),
|
||||
() => Task.FromResult(false));
|
||||
public async Task<bool> RefreshFallbackMetadata(Song song)
|
||||
{
|
||||
Option<SongMetadata> maybeMetadata = _fallbackMetadataProvider.GetFallbackMetadata(song);
|
||||
foreach (SongMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(song, metadata);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo)
|
||||
{
|
||||
Option<MusicVideoMetadata> maybeMetadata = _fallbackMetadataProvider.GetFallbackMetadata(musicVideo);
|
||||
foreach (MusicVideoMetadata metadata in maybeMetadata)
|
||||
{
|
||||
return await ApplyMetadataUpdate(musicVideo, metadata);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder) =>
|
||||
ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder));
|
||||
@@ -767,6 +827,10 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
Option<OtherVideoMetadata> maybeMetadata = Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone();
|
||||
foreach (OtherVideoMetadata existing in maybeMetadata)
|
||||
{
|
||||
existing.ContentRating = metadata.ContentRating;
|
||||
existing.Outline = metadata.Outline;
|
||||
existing.Plot = metadata.Plot;
|
||||
existing.Tagline = metadata.Tagline;
|
||||
existing.Title = metadata.Title;
|
||||
|
||||
if (existing.DateAdded == SystemTime.MinValueUtc)
|
||||
@@ -776,6 +840,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
|
||||
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;
|
||||
@@ -784,10 +851,70 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
bool updated = await UpdateMetadataCollections(
|
||||
existing,
|
||||
metadata,
|
||||
(_, _) => Task.FromResult(false),
|
||||
_otherVideoRepository.AddGenre,
|
||||
_otherVideoRepository.AddTag,
|
||||
(_, _) => Task.FromResult(false),
|
||||
(_, _) => Task.FromResult(false));
|
||||
_otherVideoRepository.AddStudio,
|
||||
_otherVideoRepository.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))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Director director in metadata.Directors
|
||||
.Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||
{
|
||||
existing.Directors.Add(director);
|
||||
if (await _otherVideoRepository.AddDirector(existing, 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))
|
||||
{
|
||||
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 _otherVideoRepository.AddWriter(existing, 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))
|
||||
{
|
||||
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))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return await _metadataRepository.Update(existing) || updated;
|
||||
}
|
||||
@@ -1068,6 +1195,81 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Option<OtherVideoMetadata>> LoadOtherVideoMetadata(string nfoFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
|
||||
Either<BaseError, OtherVideoNfo> maybeNfo = await _otherVideoNfoReader.Read(fileStream);
|
||||
foreach (BaseError error in maybeNfo.LeftToSeq())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Failed to read OtherVideo nfo metadata from {Path}: {Error}",
|
||||
nfoFileName,
|
||||
error.ToString());
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
foreach (OtherVideoNfo nfo in maybeNfo.RightToSeq())
|
||||
{
|
||||
DateTime dateAdded = DateTime.UtcNow;
|
||||
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
|
||||
|
||||
var year = 0;
|
||||
if (nfo.Year > 1000)
|
||||
{
|
||||
year = nfo.Year;
|
||||
}
|
||||
|
||||
DateTime releaseDate = year > 0
|
||||
? new DateTimeOffset(year, 1, 1, 0, 0, 0, TimeSpan.Zero).UtcDateTime
|
||||
: SystemTime.MinValueUtc;
|
||||
|
||||
foreach (DateTime premiered in nfo.Premiered)
|
||||
{
|
||||
if (year == 0)
|
||||
{
|
||||
year = premiered.Year;
|
||||
}
|
||||
|
||||
releaseDate = premiered;
|
||||
}
|
||||
|
||||
return new OtherVideoMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Sidecar,
|
||||
DateAdded = dateAdded,
|
||||
DateUpdated = dateUpdated,
|
||||
Title = nfo.Title,
|
||||
SortTitle = nfo.SortTitle,
|
||||
Year = year,
|
||||
ContentRating = nfo.ContentRating,
|
||||
ReleaseDate = releaseDate,
|
||||
Plot = nfo.Plot,
|
||||
Outline = nfo.Outline,
|
||||
Tagline = nfo.Tagline,
|
||||
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
|
||||
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
|
||||
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(),
|
||||
Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
|
||||
Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(),
|
||||
Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(),
|
||||
Guids = nfo.UniqueIds
|
||||
.Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" })
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read OtherVideo nfo metadata from {Path}", nfoFileName);
|
||||
_client.Notify(ex);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private static int? GetYear(int? year, Option<DateTime> premiered)
|
||||
{
|
||||
if (year is > 1000)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo;
|
||||
|
||||
[XmlRoot("movie")]
|
||||
public class OtherVideoNfo
|
||||
{
|
||||
[XmlElement("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[XmlElement("sorttitle")]
|
||||
public string SortTitle { get; set; }
|
||||
|
||||
[XmlElement("outline")]
|
||||
public string Outline { get; set; }
|
||||
|
||||
[XmlElement("year")]
|
||||
public int Year { get; set; }
|
||||
|
||||
[XmlElement("mpaa")]
|
||||
public string ContentRating { get; set; }
|
||||
|
||||
[XmlElement("premiered")]
|
||||
public Option<DateTime> Premiered { get; set; }
|
||||
|
||||
[XmlElement("plot")]
|
||||
public string Plot { get; set; }
|
||||
|
||||
[XmlElement("tagline")]
|
||||
public string Tagline { get; set; }
|
||||
|
||||
[XmlElement("genre")]
|
||||
public List<string> Genres { get; set; }
|
||||
|
||||
[XmlElement("tag")]
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
[XmlElement("studio")]
|
||||
public List<string> Studios { get; set; }
|
||||
|
||||
[XmlElement("actor")]
|
||||
public List<ActorNfo> Actors { get; set; }
|
||||
|
||||
[XmlElement("credits")]
|
||||
public List<string> Writers { get; set; }
|
||||
|
||||
[XmlElement("director")]
|
||||
public List<string> Directors { get; set; }
|
||||
|
||||
[XmlElement("uniqueid")]
|
||||
public List<UniqueIdNfo> UniqueIds { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Xml;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo;
|
||||
|
||||
public class OtherVideoNfoReader : NfoReader<OtherVideoNfo>, IOtherVideoNfoReader
|
||||
{
|
||||
private readonly IClient _client;
|
||||
|
||||
public OtherVideoNfoReader(IClient client) => _client = client;
|
||||
|
||||
public async Task<Either<BaseError, OtherVideoNfo>> Read(Stream input)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
|
||||
using var reader = XmlReader.Create(input, settings);
|
||||
OtherVideoNfo nfo = null;
|
||||
var done = false;
|
||||
|
||||
while (!done && await reader.ReadAsync())
|
||||
{
|
||||
switch (reader.NodeType)
|
||||
{
|
||||
case XmlNodeType.Element:
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "movie":
|
||||
nfo = new OtherVideoNfo
|
||||
{
|
||||
Genres = new List<string>(),
|
||||
Tags = new List<string>(),
|
||||
Studios = new List<string>(),
|
||||
Actors = new List<ActorNfo>(),
|
||||
Writers = new List<string>(),
|
||||
Directors = new List<string>(),
|
||||
UniqueIds = new List<UniqueIdNfo>()
|
||||
};
|
||||
break;
|
||||
case "title":
|
||||
await ReadStringContent(reader, nfo, (movie, title) => movie.Title = title);
|
||||
break;
|
||||
case "sorttitle":
|
||||
await ReadStringContent(reader, nfo, (movie, sortTitle) => movie.SortTitle = sortTitle);
|
||||
break;
|
||||
case "outline":
|
||||
await ReadStringContent(reader, nfo, (movie, outline) => movie.Outline = outline);
|
||||
break;
|
||||
case "year":
|
||||
await ReadIntContent(reader, nfo, (movie, year) => movie.Year = year);
|
||||
break;
|
||||
case "mpaa":
|
||||
await ReadStringContent(
|
||||
reader,
|
||||
nfo,
|
||||
(movie, contentRating) => movie.ContentRating = contentRating);
|
||||
break;
|
||||
case "premiered":
|
||||
await ReadDateTimeContent(
|
||||
reader,
|
||||
nfo,
|
||||
(movie, premiered) => movie.Premiered = premiered);
|
||||
break;
|
||||
case "plot":
|
||||
await ReadStringContent(reader, nfo, (movie, plot) => movie.Plot = plot);
|
||||
break;
|
||||
case "genre":
|
||||
await ReadStringContent(reader, nfo, (movie, genre) => movie.Genres.Add(genre));
|
||||
break;
|
||||
case "tag":
|
||||
await ReadStringContent(reader, nfo, (movie, tag) => movie.Tags.Add(tag));
|
||||
break;
|
||||
case "studio":
|
||||
await ReadStringContent(reader, nfo, (movie, studio) => movie.Studios.Add(studio));
|
||||
break;
|
||||
case "actor":
|
||||
ReadActor(reader, nfo, (movie, actor) => movie.Actors.Add(actor));
|
||||
break;
|
||||
case "credits":
|
||||
await ReadStringContent(reader, nfo, (movie, writer) => movie.Writers.Add(writer));
|
||||
break;
|
||||
case "director":
|
||||
await ReadStringContent(
|
||||
reader,
|
||||
nfo,
|
||||
(movie, director) => movie.Directors.Add(director));
|
||||
break;
|
||||
case "uniqueid":
|
||||
await ReadUniqueId(reader, nfo, (movie, uniqueid) => movie.UniqueIds.Add(uniqueid));
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case XmlNodeType.EndElement:
|
||||
if (reader.Name == "movie")
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Optional(nfo).ToEither((BaseError)new FailedToReadNfo());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return new FailedToReadNfo(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,15 +77,17 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
|
||||
var foldersCompleted = 0;
|
||||
|
||||
var allFolders = new System.Collections.Generic.HashSet<string>();
|
||||
var folderQueue = new Queue<string>();
|
||||
|
||||
if (ShouldIncludeFolder(libraryPath.Path))
|
||||
if (ShouldIncludeFolder(libraryPath.Path) && allFolders.Add(libraryPath.Path))
|
||||
{
|
||||
folderQueue.Enqueue(libraryPath.Path);
|
||||
}
|
||||
|
||||
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path)
|
||||
.Filter(ShouldIncludeFolder)
|
||||
.Filter(allFolders.Add)
|
||||
.OrderBy(identity))
|
||||
{
|
||||
folderQueue.Enqueue(folder);
|
||||
@@ -115,6 +117,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
|
||||
foreach (string subdirectory in _localFileSystem.ListSubdirectories(otherVideoFolder)
|
||||
.Filter(ShouldIncludeFolder)
|
||||
.Filter(allFolders.Add)
|
||||
.OrderBy(identity))
|
||||
{
|
||||
folderQueue.Enqueue(subdirectory);
|
||||
@@ -205,15 +208,38 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
try
|
||||
{
|
||||
OtherVideo otherVideo = result.Item;
|
||||
if (!Optional(otherVideo.OtherVideoMetadata).Flatten().Any())
|
||||
{
|
||||
otherVideo.OtherVideoMetadata ??= new List<OtherVideoMetadata>();
|
||||
string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
|
||||
string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
|
||||
if (await _localMetadataProvider.RefreshFallbackMetadata(otherVideo))
|
||||
Option<string> maybeNfoFile = new List<string> { Path.ChangeExtension(path, "nfo") }
|
||||
.Filter(_localFileSystem.FileExists)
|
||||
.HeadOrNone();
|
||||
|
||||
if (maybeNfoFile.IsNone)
|
||||
{
|
||||
if (!Optional(otherVideo.OtherVideoMetadata).Flatten().Any())
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
|
||||
if (await _localMetadataProvider.RefreshFallbackMetadata(otherVideo))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string nfoFile in maybeNfoFile)
|
||||
{
|
||||
bool shouldUpdate = Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone().Match(
|
||||
m => m.MetadataKind == MetadataKind.Fallback ||
|
||||
m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile),
|
||||
true);
|
||||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
|
||||
if (await _localMetadataProvider.RefreshSidecarMetadata(otherVideo, nfoFile))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-2
@@ -10,11 +10,35 @@ public class OtherVideoMetadataConfiguration : IEntityTypeConfiguration<OtherVid
|
||||
{
|
||||
builder.ToTable("OtherVideoMetadata");
|
||||
|
||||
builder.HasMany(mm => mm.Artwork)
|
||||
builder.HasMany(ovm => ovm.Artwork)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(mm => mm.Tags)
|
||||
builder.HasMany(ovm => ovm.Genres)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(ovm => ovm.Tags)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(ovm => ovm.Studios)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(ovm => ovm.Actors)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(ovm => ovm.Directors)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(ovm => ovm.Writers)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(ovm => ovm.Guids)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
|
||||
@@ -465,6 +465,10 @@ public class MetadataRepository : IMetadataRepository
|
||||
await dbContext.Connection.ExecuteAsync(
|
||||
"INSERT INTO MetadataGuid (Guid, ArtistMetadataId) VALUES (@Guid, @MetadataId)",
|
||||
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
|
||||
OtherVideoMetadata =>
|
||||
await dbContext.Connection.ExecuteAsync(
|
||||
"INSERT INTO MetadataGuid (Guid, OtherVideoMetadataId) VALUES (@Guid, @MetadataId)",
|
||||
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,10 +20,23 @@ public class OtherVideoRepository : IOtherVideoRepository
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
Option<OtherVideo> maybeExisting = await dbContext.OtherVideos
|
||||
.AsNoTracking()
|
||||
.Include(ov => ov.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Artwork)
|
||||
.Include(ov => ov.OtherVideoMetadata)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Genres)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Tags)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Studios)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Guids)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Actors)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Actors)
|
||||
.ThenInclude(a => a.Artwork)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Directors)
|
||||
.Include(i => i.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Writers)
|
||||
.Include(ov => ov.LibraryPath)
|
||||
.ThenInclude(lp => lp.Library)
|
||||
.Include(ov => ov.MediaVersions)
|
||||
@@ -82,6 +95,14 @@ public class OtherVideoRepository : IOtherVideoRepository
|
||||
return ids;
|
||||
}
|
||||
|
||||
public async Task<bool> AddGenre(OtherVideoMetadata metadata, Genre genre)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
"INSERT INTO Genre (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
}
|
||||
|
||||
public async Task<bool> AddTag(OtherVideoMetadata metadata, Tag tag)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@@ -90,6 +111,57 @@ public class OtherVideoRepository : IOtherVideoRepository
|
||||
new { tag.Name, MetadataId = metadata.Id, tag.ExternalCollectionId }).Map(result => result > 0);
|
||||
}
|
||||
|
||||
public async Task<bool> AddStudio(OtherVideoMetadata metadata, Studio studio)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
"INSERT INTO Studio (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
}
|
||||
|
||||
public async Task<bool> AddActor(OtherVideoMetadata metadata, Actor actor)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
int? artworkId = null;
|
||||
|
||||
if (actor.Artwork != null)
|
||||
{
|
||||
artworkId = await dbContext.Connection.QuerySingleAsync<int>(
|
||||
@"INSERT INTO Artwork (ArtworkKind, DateAdded, DateUpdated, Path)
|
||||
VALUES (@ArtworkKind, @DateAdded, @DateUpdated, @Path);
|
||||
SELECT last_insert_rowid()",
|
||||
new
|
||||
{
|
||||
ArtworkKind = (int)actor.Artwork.ArtworkKind,
|
||||
actor.Artwork.DateAdded,
|
||||
actor.Artwork.DateUpdated,
|
||||
actor.Artwork.Path
|
||||
});
|
||||
}
|
||||
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
"INSERT INTO Actor (Name, Role, \"Order\", OtherVideoMetadataId, ArtworkId) VALUES (@Name, @Role, @Order, @MetadataId, @ArtworkId)",
|
||||
new { actor.Name, actor.Role, actor.Order, MetadataId = metadata.Id, ArtworkId = artworkId })
|
||||
.Map(result => result > 0);
|
||||
}
|
||||
|
||||
public async Task<bool> AddDirector(OtherVideoMetadata metadata, Director director)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
"INSERT INTO Director (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
}
|
||||
|
||||
public async Task<bool> AddWriter(OtherVideoMetadata metadata, Writer writer)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
"INSERT INTO Writer (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
}
|
||||
|
||||
public async Task<List<OtherVideoMetadata>> GetOtherVideosForCards(List<int> ids)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
@@ -98,9 +98,19 @@ public class SearchRepository : ISearchRepository
|
||||
.Include(mi => (mi as Artist).ArtistMetadata)
|
||||
.ThenInclude(mm => mm.Moods)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(mm => mm.Tags)
|
||||
.ThenInclude(ovm => ovm.Genres)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Tags)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Studios)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Actors)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Directors)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Writers)
|
||||
.Include(mi => (mi as OtherVideo).MediaVersions)
|
||||
.ThenInclude(mm => mm.Streams)
|
||||
.ThenInclude(ovm => ovm.Streams)
|
||||
.Include(mi => (mi as Song).SongMetadata)
|
||||
.ThenInclude(mm => mm.Tags)
|
||||
.Include(mi => (mi as Song).SongMetadata)
|
||||
@@ -242,9 +252,19 @@ public class SearchRepository : ISearchRepository
|
||||
.Include(mi => (mi as Artist).ArtistMetadata)
|
||||
.ThenInclude(mm => mm.Moods)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(mm => mm.Tags)
|
||||
.ThenInclude(ovm => ovm.Genres)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Tags)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Studios)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Actors)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Directors)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Writers)
|
||||
.Include(mi => (mi as OtherVideo).MediaVersions)
|
||||
.ThenInclude(mm => mm.Streams)
|
||||
.ThenInclude(ovm => ovm.Streams)
|
||||
.Include(mi => (mi as Song).SongMetadata)
|
||||
.ThenInclude(mm => mm.Tags)
|
||||
.Include(mi => (mi as Song).SongMetadata)
|
||||
|
||||
+4279
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Expand_OtherVideoMetadata : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Actor");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Genre");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "MetadataGuid");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Studio");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Writer",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ContentRating",
|
||||
table: "OtherVideoMetadata",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Outline",
|
||||
table: "OtherVideoMetadata",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Plot",
|
||||
table: "OtherVideoMetadata",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Tagline",
|
||||
table: "OtherVideoMetadata",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Director",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Writer_OtherVideoMetadataId",
|
||||
table: "Writer",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Director_OtherVideoMetadataId",
|
||||
table: "Director",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Actor",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Director_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Director",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Genre",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "MetadataGuid",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Studio",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Writer_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Writer",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Actor");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Director_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Director");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Genre");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "MetadataGuid");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Studio");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Writer_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Writer");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Writer_OtherVideoMetadataId",
|
||||
table: "Writer");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Director_OtherVideoMetadataId",
|
||||
table: "Director");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Writer");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ContentRating",
|
||||
table: "OtherVideoMetadata");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Outline",
|
||||
table: "OtherVideoMetadata");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Plot",
|
||||
table: "OtherVideoMetadata");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Tagline",
|
||||
table: "OtherVideoMetadata");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Director");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Actor",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Genre",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "MetadataGuid",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Studio",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,12 +388,17 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EpisodeMetadataId");
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.ToTable("Director", (string)null);
|
||||
});
|
||||
|
||||
@@ -1296,6 +1301,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ContentRating")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -1311,12 +1319,21 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int>("OtherVideoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Outline")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Plot")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Tagline")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -2142,12 +2159,17 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EpisodeMetadataId");
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.ToTable("Writer", (string)null);
|
||||
});
|
||||
|
||||
@@ -2603,7 +2625,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Actors")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Actors")
|
||||
@@ -2735,6 +2758,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.WithMany("Directors")
|
||||
.HasForeignKey("MovieMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Directors")
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b =>
|
||||
@@ -2835,7 +2863,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Genres")
|
||||
@@ -3001,7 +3030,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Guids")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Guids")
|
||||
@@ -3429,7 +3459,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Studios")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Studios")
|
||||
@@ -3573,6 +3604,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.WithMany("Writers")
|
||||
.HasForeignKey("MovieMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Writers")
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b =>
|
||||
@@ -4054,6 +4090,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.Navigation("Artwork");
|
||||
|
||||
b.Navigation("Directors");
|
||||
|
||||
b.Navigation("Genres");
|
||||
|
||||
b.Navigation("Guids");
|
||||
@@ -4063,6 +4101,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("Subtitles");
|
||||
|
||||
b.Navigation("Tags");
|
||||
|
||||
b.Navigation("Writers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
|
||||
@@ -952,13 +952,61 @@ public sealed class SearchIndex : ISearchIndex
|
||||
doc.Add(new Int32Field(WidthField, version.Width, Field.Store.NO));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
|
||||
{
|
||||
foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/")
|
||||
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||
{
|
||||
doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO));
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.ReleaseDate.HasValue)
|
||||
{
|
||||
doc.Add(
|
||||
new StringField(
|
||||
ReleaseDateField,
|
||||
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
|
||||
Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Plot))
|
||||
{
|
||||
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Genre genre in metadata.Genres)
|
||||
{
|
||||
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Tag tag in metadata.Tags)
|
||||
{
|
||||
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Studio studio in metadata.Studios)
|
||||
{
|
||||
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Actor actor in metadata.Actors)
|
||||
{
|
||||
doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Director director in metadata.Directors)
|
||||
{
|
||||
doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Writer writer in metadata.Writers)
|
||||
{
|
||||
doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
_writer.UpdateDocument(new Term(IdField, otherVideo.Id.ToString()), doc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -415,6 +415,7 @@ public class Startup
|
||||
services.AddScoped<IArtistNfoReader, ArtistNfoReader>();
|
||||
services.AddScoped<IMusicVideoNfoReader, MusicVideoNfoReader>();
|
||||
services.AddScoped<ITvShowNfoReader, TvShowNfoReader>();
|
||||
services.AddScoped<IOtherVideoNfoReader, OtherVideoNfoReader>();
|
||||
|
||||
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Want to join the community or have a question? Join us on [Discord](https://disc
|
||||
- [Emby](https://emby.media/) media and metadata
|
||||
- Song and music video libraries
|
||||
- Pre-roll, mid-roll, post-roll filler options
|
||||
- Picture-based subtitle burn-in
|
||||
- Subtitle burn-in
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -13,12 +13,19 @@ Each movie folder may contain a `movie.nfo` file, or an NFO file with exactly th
|
||||
ErsatzTV will read the following fields from the movie NFO:
|
||||
|
||||
- Title
|
||||
- Sort Title
|
||||
- Outline
|
||||
- Year
|
||||
- MPAA
|
||||
- Premiered
|
||||
- Plot
|
||||
- Genre(s)
|
||||
- Tag(s)
|
||||
- Studio(s)
|
||||
- Actor(s)
|
||||
- Credit(s)
|
||||
- Director(s)
|
||||
- Unique Id(s)
|
||||
|
||||
### Movie Fallback Metadata
|
||||
|
||||
@@ -115,6 +122,26 @@ When no artist NFO is found, the artist metadata will only contain a name, which
|
||||
|
||||
The `Other Videos` library has no folder requirements, but folders can be a useful source of metadata.
|
||||
|
||||
### NFO Metadata
|
||||
|
||||
Each other video may have a corresponding NFO file with exactly the same name, except for the `.nfo` extension. The NFO must use the movie format. See [Kodi Wiki](https://kodi.wiki/view/NFO_files/Movies) for more information.
|
||||
ErsatzTV will read the following fields from the other video NFO:
|
||||
|
||||
- Title
|
||||
- Sort Title
|
||||
- Outline
|
||||
- Year
|
||||
- MPAA
|
||||
- Premiered
|
||||
- Plot
|
||||
- Genre(s)
|
||||
- Tag(s)
|
||||
- Studio(s)
|
||||
- Actor(s)
|
||||
- Credit(s)
|
||||
- Director(s)
|
||||
- Unique Id(s)
|
||||
|
||||
### Other Video Fallback Metadata
|
||||
|
||||
Other videos will have a tag added to their metadata for every containing folder, including the top-level folder. As an example, consider adding a commercials folder with the following files:
|
||||
|
||||
@@ -114,12 +114,22 @@ The following fields are available for searching music videos:
|
||||
|
||||
The following fields are available for searching other videos:
|
||||
|
||||
- `title`: The filename of the video (without extension)
|
||||
- `tag`: All of the video's parent folders
|
||||
- `minutes`: the rounded-up whole number duration of the video in minutes
|
||||
- `added_date`: The date the other video was added to ErsatzTV (YYYYMMDD)
|
||||
- `height`: The other video height
|
||||
- `width`: The other video width
|
||||
- `title`: The NFO title or the filename of the video (without extension)
|
||||
- `genre`: The video genre
|
||||
- `tag`: The video tag
|
||||
- `plot`: The video plot
|
||||
- `studio`: The video studio
|
||||
- `actor`: An actor from the video
|
||||
- `director`: A director from the video
|
||||
- `writer`: A writer from the video
|
||||
- `library_name`: The name of the library that contains the video
|
||||
- `content_rating`: The video content rating (case-sensitive)
|
||||
- `language`: The video audio stream language
|
||||
- `release_date`: The video release date (YYYYMMDD)
|
||||
- `added_date`: The date the video was added to ErsatzTV (YYYYMMDD)
|
||||
- `minutes`: The rounded-up whole number duration of the video in minutes
|
||||
- `height`: The video height
|
||||
- `width`: The video width
|
||||
- `type`: Always `other_video`
|
||||
|
||||
### Songs
|
||||
|
||||
Reference in New Issue
Block a user