Browse Source

metadata improvements (#95)

* fix episode fallback metadata processing, fix show fallback metadata year parsing

* fix sort title for "a" and "an"

* add and index studio metadata

* minimize circular logging with search index errors

* update plex movie sort titles as needed

* properly escape search links

* force refreshing all movie/show metadata
pull/96/head v0.0.23-prealpha
Jason Dove 5 years ago committed by GitHub
parent
commit
4c70d61d48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      ErsatzTV.Application/MediaCards/Queries/GetSearchCards.cs
  2. 43
      ErsatzTV.Application/MediaCards/Queries/GetSearchCardsHandler.cs
  3. 3
      ErsatzTV.Application/Movies/Mapper.cs
  4. 3
      ErsatzTV.Application/Movies/MovieViewModel.cs
  5. 3
      ErsatzTV.Application/Television/Mapper.cs
  6. 3
      ErsatzTV.Application/Television/TelevisionShowViewModel.cs
  7. 3
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  8. 1
      ErsatzTV.Core/Domain/Metadata/Metadata.cs
  9. 8
      ErsatzTV.Core/Domain/Metadata/Studio.cs
  10. 2
      ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs
  11. 3
      ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs
  12. 2
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  13. 16
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  14. 102
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  15. 4
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  16. 16
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  17. 31
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  18. 22
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  19. 4
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs
  20. 8
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/ShowMetadataConfiguration.cs
  21. 11
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/StudioConfiguration.cs
  22. 8
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  23. 21
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  24. 4
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  25. 36
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  26. 1749
      ErsatzTV.Infrastructure/Migrations/20210321162257_Add_Studio.Designer.cs
  27. 75
      ErsatzTV.Infrastructure/Migrations/20210321162257_Add_Studio.cs
  28. 1749
      ErsatzTV.Infrastructure/Migrations/20210321182241_Reset_MetadataDateUpdated_Studio.Designer.cs
  29. 18
      ErsatzTV.Infrastructure/Migrations/20210321182241_Reset_MetadataDateUpdated_Studio.cs
  30. 67
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  31. 1
      ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs
  32. 16
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  33. 15
      ErsatzTV.Infrastructure/Search/SearchIndex.cs
  34. 14
      ErsatzTV/Pages/Movie.razor
  35. 14
      ErsatzTV/Pages/TelevisionSeasonList.razor

8
ErsatzTV.Application/MediaCards/Queries/GetSearchCards.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetSearchCards(string Query) : IRequest<Either<BaseError, SearchCardResultsViewModel>>;
}

43
ErsatzTV.Application/MediaCards/Queries/GetSearchCardsHandler.cs

@ -1,43 +0,0 @@ @@ -1,43 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class GetSearchCardsHandler : IRequestHandler<GetSearchCards, Either<BaseError, SearchCardResultsViewModel>>
{
private readonly ISearchRepository _searchRepository;
public GetSearchCardsHandler(ISearchRepository searchRepository) => _searchRepository = searchRepository;
public Task<Either<BaseError, SearchCardResultsViewModel>> Handle(
GetSearchCards request,
CancellationToken cancellationToken) =>
request.Query.Split(":").Head() switch
{
"genre" => GenreSearch(request.Query.Replace("genre:", string.Empty)),
"tag" => TagSearch(request.Query.Replace("tag:", string.Empty)),
_ => TitleSearch(request.Query)
};
private Task<Either<BaseError, SearchCardResultsViewModel>> TitleSearch(string query) =>
Try(_searchRepository.SearchMediaItemsByTitle(query)).Sequence()
.Map(ProjectToSearchResults)
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
private Task<Either<BaseError, SearchCardResultsViewModel>> GenreSearch(string query) =>
Try(_searchRepository.SearchMediaItemsByGenre(query)).Sequence()
.Map(ProjectToSearchResults)
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
private Task<Either<BaseError, SearchCardResultsViewModel>> TagSearch(string query) =>
Try(_searchRepository.SearchMediaItemsByTag(query)).Sequence()
.Map(ProjectToSearchResults)
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
}
}

3
ErsatzTV.Application/Movies/Mapper.cs

@ -16,7 +16,8 @@ namespace ErsatzTV.Application.Movies @@ -16,7 +16,8 @@ namespace ErsatzTV.Application.Movies
Artwork(metadata, ArtworkKind.Poster),
Artwork(metadata, ArtworkKind.FanArt),
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Tags.Map(t => t.Name).ToList());
metadata.Tags.Map(t => t.Name).ToList(),
metadata.Studios.Map(s => s.Name).ToList());
}
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>

3
ErsatzTV.Application/Movies/MovieViewModel.cs

@ -9,5 +9,6 @@ namespace ErsatzTV.Application.Movies @@ -9,5 +9,6 @@ namespace ErsatzTV.Application.Movies
string Poster,
string FanArt,
List<string> Genres,
List<string> Tags);
List<string> Tags,
List<string> Studios);
}

3
ErsatzTV.Application/Television/Mapper.cs

@ -16,7 +16,8 @@ namespace ErsatzTV.Application.Television @@ -16,7 +16,8 @@ namespace ErsatzTV.Application.Television
show.ShowMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(GetFanArt).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List<string>()),
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()));
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()),
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList()).IfNone(new List<string>()));
internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
new(

3
ErsatzTV.Application/Television/TelevisionShowViewModel.cs

@ -10,5 +10,6 @@ namespace ErsatzTV.Application.Television @@ -10,5 +10,6 @@ namespace ErsatzTV.Application.Television
string Poster,
string FanArt,
List<string> Genres,
List<string> Tags);
List<string> Tags,
List<string> Studios);
}

3
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -76,6 +76,9 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -76,6 +76,9 @@ namespace ErsatzTV.Core.Tests.Fakes
throw new NotSupportedException();
public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException();
public Task<bool> AddTag(ShowMetadata metadata, Tag tag) => throw new NotSupportedException();
public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException();
public Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys) =>
throw new NotSupportedException();

1
ErsatzTV.Core/Domain/Metadata/Metadata.cs

@ -17,5 +17,6 @@ namespace ErsatzTV.Core.Domain @@ -17,5 +17,6 @@ namespace ErsatzTV.Core.Domain
public List<Artwork> Artwork { get; set; }
public List<Genre> Genres { get; set; }
public List<Tag> Tags { get; set; }
public List<Studio> Studios { get; set; }
}
}

8
ErsatzTV.Core/Domain/Metadata/Studio.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Domain
{
public class Studio
{
public int Id { get; set; }
public string Name { get; set; }
}
}

2
ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs

@ -7,6 +7,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -7,6 +7,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public interface IMetadataRepository
{
Task<bool> RemoveGenre(Genre genre);
Task<bool> RemoveTag(Tag tag);
Task<bool> RemoveStudio(Studio studio);
Task<bool> Update(Domain.Metadata metadata);
Task<bool> Add(Domain.Metadata metadata);
Task<bool> UpdateLocalStatistics(MediaVersion mediaVersion);

3
ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs

@ -18,6 +18,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -18,6 +18,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
Task<bool> AddGenre(MovieMetadata metadata, Genre genre);
Task<bool> AddTag(MovieMetadata metadata, Tag tag);
Task<bool> AddStudio(MovieMetadata metadata, Studio studio);
Task<List<int>> RemoveMissingPlexMovies(PlexLibrary library, List<string> movieKeys);
Task<bool> UpdateSortTitle(MovieMetadata movieMetadata);
}
}

2
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -40,6 +40,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -40,6 +40,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item);
Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item);
Task<bool> AddGenre(ShowMetadata metadata, Genre genre);
Task<bool> AddTag(ShowMetadata metadata, Tag tag);
Task<bool> AddStudio(ShowMetadata metadata, Studio studio);
Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys);
Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys);

16
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -23,7 +23,7 @@ namespace ErsatzTV.Core.Metadata @@ -23,7 +23,7 @@ namespace ErsatzTV.Core.Metadata
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
string fileName = Path.GetFileName(path);
var metadata = new EpisodeMetadata
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? path };
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, DateAdded = DateTime.UtcNow };
return fileName != null ? GetEpisodeMetadata(fileName, metadata) : Tuple(metadata, 0);
}
@ -48,6 +48,16 @@ namespace ErsatzTV.Core.Metadata @@ -48,6 +48,16 @@ namespace ErsatzTV.Core.Metadata
return title.Substring(4);
}
if (title.StartsWith("a ", StringComparison.OrdinalIgnoreCase))
{
return title.Substring(2);
}
if (title.StartsWith("an ", StringComparison.OrdinalIgnoreCase))
{
return title.Substring(3);
}
if (title.StartsWith("Æ"))
{
return title.Replace("Æ", "E");
@ -65,6 +75,7 @@ namespace ErsatzTV.Core.Metadata @@ -65,6 +75,7 @@ namespace ErsatzTV.Core.Metadata
if (match.Success)
{
metadata.Title = match.Groups[1].Value;
metadata.DateUpdated = DateTime.UtcNow;
return Tuple(metadata, int.Parse(match.Groups[3].Value));
}
}
@ -89,6 +100,8 @@ namespace ErsatzTV.Core.Metadata @@ -89,6 +100,8 @@ namespace ErsatzTV.Core.Metadata
metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
metadata.Genres = new List<Genre>();
metadata.Tags = new List<Tag>();
metadata.Studios = new List<Studio>();
metadata.DateUpdated = DateTime.UtcNow;
}
}
catch (Exception)
@ -110,6 +123,7 @@ namespace ErsatzTV.Core.Metadata @@ -110,6 +123,7 @@ namespace ErsatzTV.Core.Metadata
metadata.Title = match.Groups[1].Value;
metadata.Year = int.Parse(match.Groups[2].Value);
metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
metadata.DateUpdated = DateTime.UtcNow;
}
}
catch (Exception)

102
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -23,16 +23,19 @@ namespace ErsatzTV.Core.Metadata @@ -23,16 +23,19 @@ namespace ErsatzTV.Core.Metadata
private readonly ILogger<LocalMetadataProvider> _logger;
private readonly IMetadataRepository _metadataRepository;
private readonly IMovieRepository _movieRepository;
private readonly ITelevisionRepository _televisionRepository;
public LocalMetadataProvider(
IMetadataRepository metadataRepository,
IMovieRepository movieRepository,
ITelevisionRepository televisionRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILocalFileSystem localFileSystem,
ILogger<LocalMetadataProvider> logger)
{
_metadataRepository = metadataRepository;
_movieRepository = movieRepository;
_televisionRepository = televisionRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_localFileSystem = localFileSystem;
@ -138,7 +141,7 @@ namespace ErsatzTV.Core.Metadata @@ -138,7 +141,7 @@ namespace ErsatzTV.Core.Metadata
private Task<bool> ApplyMetadataUpdate(Movie movie, MovieMetadata metadata) =>
Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match(
existing =>
async existing =>
{
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
@ -158,29 +161,49 @@ namespace ErsatzTV.Core.Metadata @@ -158,29 +161,49 @@ namespace ErsatzTV.Core.Metadata
.ToList())
{
existing.Genres.Remove(genre);
await _metadataRepository.RemoveGenre(genre);
}
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
await _movieRepository.AddGenre(existing, genre);
}
foreach (Tag tag in existing.Tags.Filter(t => metadata.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Remove(tag);
await _metadataRepository.RemoveTag(tag);
}
foreach (Tag tag in metadata.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Add(tag);
await _movieRepository.AddTag(existing, tag);
}
return _metadataRepository.Update(existing);
foreach (Studio studio in existing.Studios
.Filter(s => metadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Remove(studio);
await _metadataRepository.RemoveStudio(studio);
}
foreach (Studio studio in metadata.Studios
.Filter(s => existing.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Add(studio);
await _movieRepository.AddStudio(existing, studio);
}
return await _metadataRepository.Update(existing);
},
() =>
async () =>
{
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
@ -188,12 +211,12 @@ namespace ErsatzTV.Core.Metadata @@ -188,12 +211,12 @@ namespace ErsatzTV.Core.Metadata
metadata.MovieId = movie.Id;
movie.MovieMetadata = new List<MovieMetadata> { metadata };
return _metadataRepository.Add(metadata);
return await _metadataRepository.Add(metadata);
});
private Task<bool> ApplyMetadataUpdate(Show show, ShowMetadata metadata) =>
Optional(show.ShowMetadata).Flatten().HeadOrNone().Match(
existing =>
async existing =>
{
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
@ -213,29 +236,49 @@ namespace ErsatzTV.Core.Metadata @@ -213,29 +236,49 @@ namespace ErsatzTV.Core.Metadata
.ToList())
{
existing.Genres.Remove(genre);
await _metadataRepository.RemoveGenre(genre);
}
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
await _televisionRepository.AddGenre(existing, genre);
}
foreach (Tag tag in existing.Tags.Filter(t => metadata.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Remove(tag);
await _metadataRepository.RemoveTag(tag);
}
foreach (Tag tag in metadata.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Add(tag);
await _televisionRepository.AddTag(existing, tag);
}
return _metadataRepository.Update(existing);
foreach (Studio studio in existing.Studios
.Filter(s => metadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Remove(studio);
await _metadataRepository.RemoveStudio(studio);
}
foreach (Studio studio in metadata.Studios
.Filter(s => existing.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Add(studio);
await _televisionRepository.AddStudio(existing, studio);
}
return await _metadataRepository.Update(existing);
},
() =>
async () =>
{
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
@ -243,7 +286,7 @@ namespace ErsatzTV.Core.Metadata @@ -243,7 +286,7 @@ namespace ErsatzTV.Core.Metadata
metadata.ShowId = show.Id;
show.ShowMetadata = new List<ShowMetadata> { metadata };
return _metadataRepository.Add(metadata);
return await _metadataRepository.Add(metadata);
});
private async Task<Option<MovieMetadata>> LoadMetadata(Movie mediaItem, string nfoFileName)
@ -294,10 +337,11 @@ namespace ErsatzTV.Core.Metadata @@ -294,10 +337,11 @@ namespace ErsatzTV.Core.Metadata
Plot = nfo.Plot,
Outline = nfo.Outline,
Tagline = nfo.Tagline,
Year = nfo.Year,
ReleaseDate = GetAired(nfo.Premiered) ?? new DateTime(nfo.Year, 1, 1),
Year = GetYear(nfo.Year, nfo.Premiered),
ReleaseDate = GetAired(nfo.Year, nfo.Premiered),
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList()
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList()
},
None);
}
@ -322,7 +366,7 @@ namespace ErsatzTV.Core.Metadata @@ -322,7 +366,7 @@ namespace ErsatzTV.Core.Metadata
MetadataKind = MetadataKind.Sidecar,
DateUpdated = File.GetLastWriteTimeUtc(nfoFileName),
Title = nfo.Title,
ReleaseDate = GetAired(nfo.Aired),
ReleaseDate = GetAired(0, nfo.Aired),
Plot = nfo.Plot
};
return Tuple(metadata, nfo.Episode);
@ -354,7 +398,8 @@ namespace ErsatzTV.Core.Metadata @@ -354,7 +398,8 @@ namespace ErsatzTV.Core.Metadata
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()
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList()
},
None);
}
@ -365,21 +410,38 @@ namespace ErsatzTV.Core.Metadata @@ -365,21 +410,38 @@ namespace ErsatzTV.Core.Metadata
}
}
private static DateTime? GetAired(string aired)
private static int? GetYear(int year, string premiered)
{
if (string.IsNullOrWhiteSpace(aired))
if (year > 1000)
{
return year;
}
if (string.IsNullOrWhiteSpace(premiered))
{
return null;
}
if (DateTime.TryParse(aired, out DateTime parsed))
if (DateTime.TryParse(premiered, out DateTime parsed))
{
return parsed;
return parsed.Year;
}
return null;
}
private static DateTime? GetAired(int year, string aired)
{
DateTime? fallback = year > 1000 ? new DateTime(year, 1, 1) : null;
if (string.IsNullOrWhiteSpace(aired))
{
return fallback;
}
return DateTime.TryParse(aired, out DateTime parsed) ? parsed : fallback;
}
[XmlRoot("movie")]
public class MovieNfo
{
@ -409,6 +471,9 @@ namespace ErsatzTV.Core.Metadata @@ -409,6 +471,9 @@ namespace ErsatzTV.Core.Metadata
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}
[XmlRoot("tvshow")]
@ -437,6 +502,9 @@ namespace ErsatzTV.Core.Metadata @@ -437,6 +502,9 @@ namespace ErsatzTV.Core.Metadata
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}
[XmlRoot("episodedetails")]

4
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -159,7 +159,7 @@ namespace ErsatzTV.Core.Metadata @@ -159,7 +159,7 @@ namespace ErsatzTV.Core.Metadata
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
return BaseError.New(ex.ToString());
}
}
@ -181,7 +181,7 @@ namespace ErsatzTV.Core.Metadata @@ -181,7 +181,7 @@ namespace ErsatzTV.Core.Metadata
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
return BaseError.New(ex.ToString());
}
}

16
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -200,7 +200,7 @@ namespace ErsatzTV.Core.Metadata @@ -200,7 +200,7 @@ namespace ErsatzTV.Core.Metadata
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
return BaseError.New(ex.ToString());
}
}
@ -225,7 +225,11 @@ namespace ErsatzTV.Core.Metadata @@ -225,7 +225,11 @@ namespace ErsatzTV.Core.Metadata
},
async () =>
{
if (!Optional(episode.EpisodeMetadata).Flatten().Any())
bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
m => m.DateUpdated == DateTime.MinValue,
true);
if (shouldUpdate)
{
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
@ -237,7 +241,7 @@ namespace ErsatzTV.Core.Metadata @@ -237,7 +241,7 @@ namespace ErsatzTV.Core.Metadata
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
return BaseError.New(ex.ToString());
}
}
@ -260,7 +264,7 @@ namespace ErsatzTV.Core.Metadata @@ -260,7 +264,7 @@ namespace ErsatzTV.Core.Metadata
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
return BaseError.New(ex.ToString());
}
}
@ -279,7 +283,7 @@ namespace ErsatzTV.Core.Metadata @@ -279,7 +283,7 @@ namespace ErsatzTV.Core.Metadata
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
return BaseError.New(ex.ToString());
}
}
@ -298,7 +302,7 @@ namespace ErsatzTV.Core.Metadata @@ -298,7 +302,7 @@ namespace ErsatzTV.Core.Metadata
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
return BaseError.New(ex.ToString());
}
}

31
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -158,6 +158,37 @@ namespace ErsatzTV.Core.Plex @@ -158,6 +158,37 @@ namespace ErsatzTV.Core.Plex
}
}
foreach (Studio studio in existingMetadata.Studios
.Filter(s => incomingMetadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existingMetadata.Studios.Remove(studio);
if (await _metadataRepository.RemoveStudio(studio))
{
result.IsUpdated = true;
}
}
foreach (Studio studio in incomingMetadata.Studios
.Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existingMetadata.Studios.Add(studio);
if (await _movieRepository.AddStudio(existingMetadata, studio))
{
result.IsUpdated = true;
}
}
if (incomingMetadata.SortTitle != existingMetadata.SortTitle)
{
existingMetadata.SortTitle = incomingMetadata.SortTitle;
if (await _movieRepository.UpdateSortTitle(existingMetadata))
{
result.IsUpdated = true;
}
}
// TODO: update other metadata?
}

22
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -131,6 +131,28 @@ namespace ErsatzTV.Core.Plex @@ -131,6 +131,28 @@ namespace ErsatzTV.Core.Plex
result.IsUpdated = true;
}
}
foreach (Studio studio in existingMetadata.Studios
.Filter(s => incomingMetadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existingMetadata.Studios.Remove(studio);
if (await _metadataRepository.RemoveStudio(studio))
{
result.IsUpdated = true;
}
}
foreach (Studio studio in incomingMetadata.Studios
.Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existingMetadata.Studios.Add(studio);
if (await _televisionRepository.AddStudio(existingMetadata, studio))
{
result.IsUpdated = true;
}
}
}
return result;

4
ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs

@ -21,6 +21,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -21,6 +21,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.HasMany(mm => mm.Tags)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Studios)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}

8
ErsatzTV.Infrastructure/Data/Configurations/Metadata/ShowMetadataConfiguration.cs

@ -14,11 +14,15 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -14,11 +14,15 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Genres)
builder.HasMany(sm => sm.Genres)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Tags)
builder.HasMany(sm => sm.Tags)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(sm => sm.Studios)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}

11
ErsatzTV.Infrastructure/Data/Configurations/Metadata/StudioConfiguration.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class StudioConfiguration : IEntityTypeConfiguration<Studio>
{
public void Configure(EntityTypeBuilder<Studio> builder) => builder.ToTable("Studio");
}
}

8
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -112,5 +112,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -112,5 +112,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<bool> RemoveGenre(Genre genre) =>
_dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id })
.Map(result => result > 0);
public Task<bool> RemoveTag(Tag tag) =>
_dbConnection.ExecuteAsync("DELETE FROM Tag WHERE Id = @TagId", new { TagId = tag.Id })
.Map(result => result > 0);
public Task<bool> RemoveStudio(Studio studio) =>
_dbConnection.ExecuteAsync("DELETE FROM Studio WHERE Id = @StudioId", new { StudioId = studio.Id })
.Map(result => result > 0);
}
}

21
ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs

@ -41,6 +41,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -41,6 +41,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(m => m.Genres)
.Include(m => m.MovieMetadata)
.ThenInclude(m => m.Tags)
.Include(m => m.MovieMetadata)
.ThenInclude(m => m.Studios)
.OrderBy(m => m.Id)
.SingleOrDefaultAsync(m => m.Id == movieId)
.Map(Optional);
@ -56,6 +58,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -56,6 +58,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Genres)
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Tags)
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Studios)
.Include(i => i.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(i => i.MediaVersions)
@ -82,6 +86,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -82,6 +86,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Tags)
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Studios)
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
@ -165,6 +171,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -165,6 +171,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
"INSERT INTO Genre (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddTag(MovieMetadata metadata, Tag tag) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Tag (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
new { tag.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddStudio(MovieMetadata metadata, Studio studio) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Studio (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public async Task<List<int>> RemoveMissingPlexMovies(PlexLibrary library, List<string> movieKeys)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@ -185,6 +201,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -185,6 +201,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return ids;
}
public Task<bool> UpdateSortTitle(MovieMetadata movieMetadata) =>
_dbConnection.ExecuteAsync(
@"UPDATE MovieMetadata SET SortTitle = @SortTitle WHERE Id = @Id",
new { movieMetadata.SortTitle, movieMetadata.Id }).Map(result => result > 0);
private static async Task<Either<BaseError, MediaItemScanResult<Movie>>> AddMovie(
TvContext dbContext,
int libraryPathId,

4
ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs

@ -37,10 +37,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -37,10 +37,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Genres)
.Include(mi => (mi as Movie).MovieMetadata)
.ThenInclude(mm => mm.Tags)
.Include(mi => (mi as Movie).MovieMetadata)
.ThenInclude(mm => mm.Studios)
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Genres)
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Tags)
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Studios)
.OrderBy(mi => mi.Id)
.SingleOrDefaultAsync(mi => mi.Id == id)
.Map(Optional);

36
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -53,6 +53,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -53,6 +53,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(sm => sm.Genres)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Tags)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Studios)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync()
.Map(Optional);
@ -354,11 +356,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -354,11 +356,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Option<PlexShow> maybeExisting = await dbContext.PlexShows
.AsNoTracking()
.Include(i => i.ShowMetadata)
.ThenInclude(mm => mm.Genres)
.ThenInclude(sm => sm.Genres)
.Include(i => i.ShowMetadata)
.ThenInclude(mm => mm.Tags)
.ThenInclude(sm => sm.Tags)
.Include(i => i.ShowMetadata)
.ThenInclude(mm => mm.Artwork)
.ThenInclude(sm => sm.Studios)
.Include(i => i.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.LibraryPath)
.ThenInclude(lp => lp.Library)
.OrderBy(i => i.Key)
@ -464,9 +468,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -464,9 +468,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Genre (Name, SeasonMetadataId) VALUES (@Name, @MetadataId)",
"INSERT INTO Genre (Name, ShowMetadataId) VALUES (@Name, @MetadataId)",
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddTag(ShowMetadata metadata, Tag tag) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Tag (Name, ShowMetadataId) VALUES (@Name, @MetadataId)",
new { tag.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Studio (Name, ShowMetadataId) VALUES (@Name, @MetadataId)",
new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public async Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@ -504,6 +518,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -504,6 +518,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
SeasonMetadata = new List<SeasonMetadata>
{
new()
{
DateAdded = DateTime.UtcNow
}
}
};
await dbContext.Seasons.AddAsync(season);
@ -524,6 +541,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -524,6 +541,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
try
{
if (dbContext.MediaFiles.Any(mf => mf.Path == path))
{
return BaseError.New("Multi-episode files are not yet supported");
}
var episode = new Episode
{
LibraryPathId = libraryPathId,
@ -532,6 +554,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -532,6 +554,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
new()
{
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.MinValue,
MetadataKind = MetadataKind.Fallback
}
@ -605,6 +628,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -605,6 +628,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
try
{
if (dbContext.MediaFiles.Any(mf => mf.Path == item.MediaVersions.Head().MediaFiles.Head().Path))
{
return BaseError.New("Multi-episode files are not yet supported");
}
item.LibraryPathId = library.Paths.Head().Id;
await dbContext.PlexEpisodes.AddAsync(item);

1749
ErsatzTV.Infrastructure/Migrations/20210321162257_Add_Studio.Designer.cs generated

File diff suppressed because it is too large Load Diff

75
ErsatzTV.Infrastructure/Migrations/20210321162257_Add_Studio.cs

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_Studio : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
"Studio",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>("TEXT", nullable: true),
EpisodeMetadataId = table.Column<int>("INTEGER", nullable: true),
MovieMetadataId = table.Column<int>("INTEGER", nullable: true),
SeasonMetadataId = table.Column<int>("INTEGER", nullable: true),
ShowMetadataId = table.Column<int>("INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Studio", x => x.Id);
table.ForeignKey(
"FK_Studio_EpisodeMetadata_EpisodeMetadataId",
x => x.EpisodeMetadataId,
"EpisodeMetadata",
"Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
"FK_Studio_MovieMetadata_MovieMetadataId",
x => x.MovieMetadataId,
"MovieMetadata",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_Studio_SeasonMetadata_SeasonMetadataId",
x => x.SeasonMetadataId,
"SeasonMetadata",
"Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
"FK_Studio_ShowMetadata_ShowMetadataId",
x => x.ShowMetadataId,
"ShowMetadata",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
"IX_Studio_EpisodeMetadataId",
"Studio",
"EpisodeMetadataId");
migrationBuilder.CreateIndex(
"IX_Studio_MovieMetadataId",
"Studio",
"MovieMetadataId");
migrationBuilder.CreateIndex(
"IX_Studio_SeasonMetadataId",
"Studio",
"SeasonMetadataId");
migrationBuilder.CreateIndex(
"IX_Studio_ShowMetadataId",
"Studio",
"ShowMetadataId");
}
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropTable(
"Studio");
}
}

1749
ErsatzTV.Infrastructure/Migrations/20210321182241_Reset_MetadataDateUpdated_Studio.Designer.cs generated

File diff suppressed because it is too large Load Diff

18
ErsatzTV.Infrastructure/Migrations/20210321182241_Reset_MetadataDateUpdated_Studio.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Reset_MetadataDateUpdated_Studio : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"UPDATE MovieMetadata SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql(@"UPDATE ShowMetadata SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql(@"UPDATE Library SET LastScan = '0001-01-01 00:00:00'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

67
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -845,6 +845,42 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -845,6 +845,42 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("ShowMetadata");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Studio",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("EpisodeMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("SeasonMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("ShowMetadataId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("EpisodeMetadataId");
b.HasIndex("MovieMetadataId");
b.HasIndex("SeasonMetadataId");
b.HasIndex("ShowMetadataId");
b.ToTable("Studio");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Tag",
b =>
@ -1499,6 +1535,29 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1499,6 +1535,29 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Show");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Studio",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
.WithMany("Studios")
.HasForeignKey("EpisodeMetadataId");
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
.WithMany("Studios")
.HasForeignKey("MovieMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
.WithMany("Studios")
.HasForeignKey("SeasonMetadataId");
b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null)
.WithMany("Studios")
.HasForeignKey("ShowMetadataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Tag",
b =>
@ -1744,6 +1803,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1744,6 +1803,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Studios");
b.Navigation("Tags");
});
@ -1765,6 +1826,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1765,6 +1826,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Studios");
b.Navigation("Tags");
});
@ -1794,6 +1857,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1794,6 +1857,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Studios");
b.Navigation("Tags");
});
@ -1805,6 +1870,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1805,6 +1870,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Studios");
b.Navigation("Tags");
});

1
ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs

@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Plex.Models @@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Plex.Models
public int AddedAt { get; set; }
public int UpdatedAt { get; set; }
public int Index { get; set; }
public string Studio { get; set; }
public List<PlexMediaResponse> Media { get; set; }
public List<PlexGenreResponse> Genre { get; set; }
}

16
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -177,9 +177,15 @@ namespace ErsatzTV.Infrastructure.Plex @@ -177,9 +177,15 @@ namespace ErsatzTV.Infrastructure.Plex
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(),
Tags = new List<Tag>()
Tags = new List<Tag>(),
Studios = new List<Studio>()
};
if (!string.IsNullOrWhiteSpace(response.Studio))
{
metadata.Studios.Add(new Studio { Name = response.Studio });
}
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
@ -279,9 +285,15 @@ namespace ErsatzTV.Infrastructure.Plex @@ -279,9 +285,15 @@ namespace ErsatzTV.Infrastructure.Plex
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(),
Tags = new List<Tag>()
Tags = new List<Tag>(),
Studios = new List<Studio>()
};
if (!string.IsNullOrWhiteSpace(response.Studio))
{
metadata.Studios.Add(new Studio { Name = response.Studio });
}
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;

15
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -38,6 +38,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -38,6 +38,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string TitleAndYearField = "title_and_year";
private const string JumpLetterField = "jump_letter";
private const string ReleaseDateField = "release_date";
private const string StudioField = "studio";
private const string MovieType = "movie";
private const string ShowType = "show";
@ -48,7 +49,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -48,7 +49,7 @@ namespace ErsatzTV.Infrastructure.Search
private readonly ILogger<SearchIndex> _logger;
private readonly ISearchRepository _searchRepository;
public SearchIndex(
ILocalFileSystem localFileSystem,
ISearchRepository searchRepository,
@ -263,10 +264,16 @@ namespace ErsatzTV.Infrastructure.Search @@ -263,10 +264,16 @@ namespace ErsatzTV.Infrastructure.Search
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));
}
writer.UpdateDocument(new Term(IdField, movie.Id.ToString()), doc);
}
catch (Exception ex)
{
metadata.Movie = null;
_logger.LogWarning(ex, "Error indexing movie with metadata {@Metadata}", metadata);
}
}
@ -316,10 +323,16 @@ namespace ErsatzTV.Infrastructure.Search @@ -316,10 +323,16 @@ namespace ErsatzTV.Infrastructure.Search
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));
}
writer.UpdateDocument(new Term(IdField, show.Id.ToString()), doc);
}
catch (Exception ex)
{
metadata.Show = null;
_logger.LogWarning(ex, "Error indexing show with metadata {@Metadata}", metadata);
}
}

14
ErsatzTV/Pages/Movie.razor

@ -43,13 +43,23 @@ @@ -43,13 +43,23 @@
</div>
</div>
</div>
@if (_movie.Studios.Any())
{
<MudText GutterBottom="true">Studios</MudText>
<div class="mb-2">
@foreach (string studio in _movie.Studios.OrderBy(s => s))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@studio" Class="mr-2 mb-2" Link="@($"/search?query=studio%3a%22{Uri.EscapeDataString(studio.ToLowerInvariant())}%22")"/>
}
</div>
}
@if (_movie.Genres.Any())
{
<MudText GutterBottom="true">Genres</MudText>
<div class="mb-2">
@foreach (string genre in _movie.Genres.OrderBy(g => g))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@genre" Class="mr-2 mb-2" Link="@($"/search?query=genre%3a%22{genre.ToLowerInvariant()}%22")"/>
<MudFab Color="Color.Info" Size="Size.Small" Label="@genre" Class="mr-2 mb-2" Link="@($"/search?query=genre%3a%22{Uri.EscapeDataString(genre.ToLowerInvariant())}%22")"/>
}
</div>
}
@ -59,7 +69,7 @@ @@ -59,7 +69,7 @@
<div>
@foreach (string tag in _movie.Tags.OrderBy(t => t))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@tag" Class="mr-2 mb-2" Link="@($"/search?query=tag%3a%22{tag.ToLowerInvariant()}%22")"/>
<MudFab Color="Color.Info" Size="Size.Small" Label="@tag" Class="mr-2 mb-2" Link="@($"/search?query=tag%3a%22{Uri.EscapeDataString(tag.ToLowerInvariant())}%22")"/>
}
</div>
}

14
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -58,13 +58,23 @@ @@ -58,13 +58,23 @@
</div>
</div>
</div>
@if (_show.Studios.Any())
{
<MudText GutterBottom="true">Studios</MudText>
<div class="mb-2">
@foreach (string studio in _show.Studios.OrderBy(g => g))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@studio" Class="mr-2 mb-2" Link="@($"/search?query=studio%3a%22{Uri.EscapeDataString(studio.ToLowerInvariant())}%22")"/>
}
</div>
}
@if (_show.Genres.Any())
{
<MudText GutterBottom="true">Genres</MudText>
<div class="mb-2">
@foreach (string genre in _show.Genres.OrderBy(g => g))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@genre" Class="mr-2 mb-2" Link="@($"/search?query=genre%3a%22{genre.ToLowerInvariant()}%22")"/>
<MudFab Color="Color.Info" Size="Size.Small" Label="@genre" Class="mr-2 mb-2" Link="@($"/search?query=genre%3a%22{Uri.EscapeDataString(genre.ToLowerInvariant())}%22")"/>
}
</div>
}
@ -74,7 +84,7 @@ @@ -74,7 +84,7 @@
<div>
@foreach (string tag in _show.Tags.OrderBy(t => t))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@tag" Class="mr-2 mb-2" Link="@($"/search?query=tag%3a%22{tag.ToLowerInvariant()}%22")"/>
<MudFab Color="Color.Info" Size="Size.Small" Label="@tag" Class="mr-2 mb-2" Link="@($"/search?query=tag%3a%22{Uri.EscapeDataString(tag.ToLowerInvariant())}%22")"/>
}
</div>
}

Loading…
Cancel
Save