Browse Source

add content rating (#218)

* add new columns

* store local content ratings

* display and search content ratings

* add content_rating to search docs

* sync content rating from jellyfin, emby, plex

* force sync content rating for all libraries

* code cleanup
pull/219/head
Jason Dove 4 years ago committed by GitHub
parent
commit
68123a2f9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ErsatzTV.Application/Movies/Mapper.cs
  2. 1
      ErsatzTV.Application/Movies/MovieViewModel.cs
  3. 4
      ErsatzTV.Application/Television/Mapper.cs
  4. 1
      ErsatzTV.Application/Television/TelevisionShowViewModel.cs
  5. 2
      ErsatzTV.Core/Domain/Metadata/MetadataKind.cs
  6. 1
      ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs
  7. 1
      ErsatzTV.Core/Domain/Metadata/ShowMetadata.cs
  8. 4
      ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs
  9. 4
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  10. 2
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  11. 2
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  12. 3
      ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs
  13. 2
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  14. 13
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  15. 13
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  16. 2
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  17. 2
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  18. 20
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  19. 4
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  20. 6
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  21. 4
      ErsatzTV.Infrastructure/Emby/IEmbyApi.cs
  22. 1
      ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs
  23. 4
      ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs
  24. 10
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  25. 1
      ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs
  26. 2717
      ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.Designer.cs
  27. 50
      ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.cs
  28. 6
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  29. 1
      ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs
  30. 6
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  31. 33
      ErsatzTV.Infrastructure/Search/SearchIndex.cs
  32. 14
      ErsatzTV/Pages/Movie.razor
  33. 14
      ErsatzTV/Pages/TelevisionSeasonList.razor
  34. 2
      docs/user-guide/search.md

2
ErsatzTV.Application/Movies/Mapper.cs

@ -26,6 +26,8 @@ namespace ErsatzTV.Application.Movies @@ -26,6 +26,8 @@ namespace ErsatzTV.Application.Movies
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Tags.Map(t => t.Name).ToList(),
metadata.Studios.Map(s => s.Name).ToList(),
(metadata.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList(),
LanguagesForMovie(movie),
metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id)
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))

1
ErsatzTV.Application/Movies/MovieViewModel.cs

@ -11,6 +11,7 @@ namespace ErsatzTV.Application.Movies @@ -11,6 +11,7 @@ namespace ErsatzTV.Application.Movies
List<string> Genres,
List<string> Tags,
List<string> Studios,
List<string> ContentRatings,
List<CultureInfo> Languages,
List<ActorCardViewModel> Actors)
{

4
ErsatzTV.Application/Television/Mapper.cs

@ -30,6 +30,10 @@ namespace ErsatzTV.Application.Television @@ -30,6 +30,10 @@ namespace ErsatzTV.Application.Television
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>()),
show.ShowMetadata.HeadOrNone()
.Map(
m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone(new List<string>()),
LanguagesForShow(languages),
show.ShowMetadata.HeadOrNone()
.Map(

1
ErsatzTV.Application/Television/TelevisionShowViewModel.cs

@ -14,6 +14,7 @@ namespace ErsatzTV.Application.Television @@ -14,6 +14,7 @@ namespace ErsatzTV.Application.Television
List<string> Genres,
List<string> Tags,
List<string> Studios,
List<string> ContentRatings,
List<CultureInfo> Languages,
List<ActorCardViewModel> Actors);
}

2
ErsatzTV.Core/Domain/Metadata/MetadataKind.cs

@ -4,6 +4,6 @@ @@ -4,6 +4,6 @@
{
Fallback = 0,
Sidecar = 1,
External
External = 2
}
}

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

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
{
public class MovieMetadata : Metadata
{
public string ContentRating { get; set; }
public string Outline { get; set; }
public string Plot { get; set; }
public string Tagline { get; set; }

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

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
{
public class ShowMetadata : Metadata
{
public string ContentRating { get; set; }
public string Outline { get; set; }
public string Plot { get; set; }
public string Tagline { get; set; }

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

@ -24,5 +24,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -24,5 +24,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Unit> MarkAsUpdated(SeasonMetadata metadata, DateTime dateUpdated);
Task<Unit> MarkAsUpdated(MovieMetadata metadata, DateTime dateUpdated);
Task<Unit> MarkAsUpdated(EpisodeMetadata metadata, DateTime dateUpdated);
Task<Unit> MarkAsExternal(ShowMetadata metadata);
Task<Unit> SetContentRating(ShowMetadata metadata, string contentRating);
Task<Unit> MarkAsExternal(MovieMetadata metadata);
Task<Unit> SetContentRating(MovieMetadata metadata, string contentRating);
}
}

4
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -232,6 +232,7 @@ namespace ErsatzTV.Core.Metadata @@ -232,6 +232,7 @@ namespace ErsatzTV.Core.Metadata
Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match(
async existing =>
{
existing.ContentRating = metadata.ContentRating;
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
existing.Tagline = metadata.Tagline;
@ -276,6 +277,7 @@ namespace ErsatzTV.Core.Metadata @@ -276,6 +277,7 @@ namespace ErsatzTV.Core.Metadata
Optional(show.ShowMetadata).Flatten().HeadOrNone().Match(
async existing =>
{
existing.ContentRating = metadata.ContentRating;
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
existing.Tagline = metadata.Tagline;
@ -474,6 +476,7 @@ namespace ErsatzTV.Core.Metadata @@ -474,6 +476,7 @@ namespace ErsatzTV.Core.Metadata
Plot = nfo.Plot,
Outline = nfo.Outline,
Tagline = nfo.Tagline,
ContentRating = nfo.ContentRating,
Year = GetYear(nfo.Year, nfo.Premiered),
ReleaseDate = GetAired(nfo.Year, nfo.Premiered),
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
@ -571,6 +574,7 @@ namespace ErsatzTV.Core.Metadata @@ -571,6 +574,7 @@ namespace ErsatzTV.Core.Metadata
DateUpdated = dateUpdated,
Title = nfo.Title,
Year = nfo.Year,
ContentRating = nfo.ContentRating,
ReleaseDate = nfo.Premiered,
Plot = nfo.Plot,
Outline = nfo.Outline,

2
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -108,7 +108,7 @@ namespace ErsatzTV.Core.Metadata @@ -108,7 +108,7 @@ namespace ErsatzTV.Core.Metadata
.HeadOrNone();
// skip folder if etag matches
if (await knownFolder.Map(f => f.Etag).IfNoneAsync(string.Empty) == etag)
if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag)
{
continue;
}

2
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -252,7 +252,7 @@ namespace ErsatzTV.Core.Metadata @@ -252,7 +252,7 @@ namespace ErsatzTV.Core.Metadata
.HeadOrNone();
// skip folder if etag matches
if (await knownFolder.Map(f => f.Etag).IfNoneAsync(string.Empty) == etag)
if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag)
{
continue;
}

3
ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs

@ -21,6 +21,9 @@ namespace ErsatzTV.Core.Metadata.Nfo @@ -21,6 +21,9 @@ namespace ErsatzTV.Core.Metadata.Nfo
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("mpaa")]
public string ContentRating { get; set; }
[XmlElement("premiered")]
public string Premiered { get; set; }

2
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -158,7 +158,7 @@ namespace ErsatzTV.Core.Metadata @@ -158,7 +158,7 @@ namespace ErsatzTV.Core.Metadata
.HeadOrNone();
// skip folder if etag matches
if (await knownFolder.Map(f => f.Etag).IfNoneAsync(string.Empty) == etag)
if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag)
{
continue;
}

13
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -173,6 +173,19 @@ namespace ErsatzTV.Core.Plex @@ -173,6 +173,19 @@ namespace ErsatzTV.Core.Plex
await maybeMetadata.Match(
async fullMetadata =>
{
if (existingMetadata.MetadataKind != MetadataKind.External)
{
existingMetadata.MetadataKind = MetadataKind.External;
await _metadataRepository.MarkAsExternal(existingMetadata);
}
if (existingMetadata.ContentRating != fullMetadata.ContentRating)
{
existingMetadata.ContentRating = fullMetadata.ContentRating;
await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating);
result.IsUpdated = true;
}
foreach (Genre genre in existingMetadata.Genres
.Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())

13
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -146,6 +146,19 @@ namespace ErsatzTV.Core.Plex @@ -146,6 +146,19 @@ namespace ErsatzTV.Core.Plex
await maybeMetadata.Match(
async fullMetadata =>
{
if (existingMetadata.MetadataKind != MetadataKind.External)
{
existingMetadata.MetadataKind = MetadataKind.External;
await _metadataRepository.MarkAsExternal(existingMetadata);
}
if (existingMetadata.ContentRating != fullMetadata.ContentRating)
{
existingMetadata.ContentRating = fullMetadata.ContentRating;
await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating);
result.IsUpdated = true;
}
foreach (Genre genre in existingMetadata.Genres
.Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())

2
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -105,6 +105,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -105,6 +105,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
// metadata
ShowMetadata metadata = existing.ShowMetadata.Head();
ShowMetadata incomingMetadata = show.ShowMetadata.Head();
metadata.MetadataKind = incomingMetadata.MetadataKind;
metadata.ContentRating = incomingMetadata.ContentRating;
metadata.Title = incomingMetadata.Title;
metadata.SortTitle = incomingMetadata.SortTitle;
metadata.Plot = incomingMetadata.Plot;

2
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -105,6 +105,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -105,6 +105,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
// metadata
ShowMetadata metadata = existing.ShowMetadata.Head();
ShowMetadata incomingMetadata = show.ShowMetadata.Head();
metadata.MetadataKind = incomingMetadata.MetadataKind;
metadata.ContentRating = incomingMetadata.ContentRating;
metadata.Title = incomingMetadata.Title;
metadata.SortTitle = incomingMetadata.SortTitle;
metadata.Plot = incomingMetadata.Plot;

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

@ -228,6 +228,26 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -228,6 +228,26 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
@"UPDATE EpisodeMetadata SET DateUpdated = @DateUpdated WHERE Id = @Id",
new { DateUpdated = dateUpdated, metadata.Id }).ToUnit();
public Task<Unit> MarkAsExternal(ShowMetadata metadata) =>
_dbConnection.ExecuteAsync(
@"UPDATE ShowMetadata SET MetadataKind = @Kind WHERE Id = @Id",
new { metadata.Id, Kind = (int) MetadataKind.External }).ToUnit();
public Task<Unit> SetContentRating(ShowMetadata metadata, string contentRating) =>
_dbConnection.ExecuteAsync(
@"UPDATE ShowMetadata SET ContentRating = @ContentRating WHERE Id = @Id",
new { metadata.Id, ContentRating = contentRating }).ToUnit();
public Task<Unit> MarkAsExternal(MovieMetadata metadata) =>
_dbConnection.ExecuteAsync(
@"UPDATE MovieMetadata SET MetadataKind = @Kind WHERE Id = @Id",
new { metadata.Id, Kind = (int) MetadataKind.External }).ToUnit();
public Task<Unit> SetContentRating(MovieMetadata metadata, string contentRating) =>
_dbConnection.ExecuteAsync(
@"UPDATE MovieMetadata SET ContentRating = @ContentRating WHERE Id = @Id",
new { metadata.Id, ContentRating = contentRating }).ToUnit();
public Task<bool> RemoveGenre(Genre genre) =>
_dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id })
.Map(result => result > 0);

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

@ -327,6 +327,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -327,6 +327,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
// metadata
MovieMetadata metadata = existing.MovieMetadata.Head();
MovieMetadata incomingMetadata = movie.MovieMetadata.Head();
metadata.MetadataKind = incomingMetadata.MetadataKind;
metadata.ContentRating = incomingMetadata.ContentRating;
metadata.Title = incomingMetadata.Title;
metadata.SortTitle = incomingMetadata.SortTitle;
metadata.Plot = incomingMetadata.Plot;
@ -528,6 +530,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -528,6 +530,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
// metadata
MovieMetadata metadata = existing.MovieMetadata.Head();
MovieMetadata incomingMetadata = movie.MovieMetadata.Head();
metadata.MetadataKind = incomingMetadata.MetadataKind;
metadata.ContentRating = incomingMetadata.ContentRating;
metadata.Title = incomingMetadata.Title;
metadata.SortTitle = incomingMetadata.SortTitle;
metadata.Plot = incomingMetadata.Plot;

6
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -230,12 +230,14 @@ namespace ErsatzTV.Infrastructure.Emby @@ -230,12 +230,14 @@ namespace ErsatzTV.Infrastructure.Emby
var metadata = new MovieMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
Year = item.ProductionYear,
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
DateAdded = dateAdded,
ContentRating = item.OfficialRating,
Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
@ -324,12 +326,14 @@ namespace ErsatzTV.Infrastructure.Emby @@ -324,12 +326,14 @@ namespace ErsatzTV.Infrastructure.Emby
var metadata = new ShowMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
Year = item.ProductionYear,
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
DateAdded = dateAdded,
ContentRating = item.OfficialRating,
Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
@ -393,6 +397,7 @@ namespace ErsatzTV.Infrastructure.Emby @@ -393,6 +397,7 @@ namespace ErsatzTV.Infrastructure.Emby
var metadata = new SeasonMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Year = item.ProductionYear,
@ -498,6 +503,7 @@ namespace ErsatzTV.Infrastructure.Emby @@ -498,6 +503,7 @@ namespace ErsatzTV.Infrastructure.Emby
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,

4
ErsatzTV.Infrastructure/Emby/IEmbyApi.cs

@ -28,7 +28,7 @@ namespace ErsatzTV.Infrastructure.Emby @@ -28,7 +28,7 @@ namespace ErsatzTV.Infrastructure.Emby
string parentId,
[Query]
string fields =
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources",
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating",
[Query]
string includeItemTypes = "Movie");
@ -40,7 +40,7 @@ namespace ErsatzTV.Infrastructure.Emby @@ -40,7 +40,7 @@ namespace ErsatzTV.Infrastructure.Emby
string parentId,
[Query]
string fields =
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources",
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating",
[Query]
string includeItemTypes = "Series");

1
ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.Infrastructure.Emby.Models @@ -9,6 +9,7 @@ namespace ErsatzTV.Infrastructure.Emby.Models
public string Id { get; set; }
public string Etag { get; set; }
public string Path { get; set; }
public string OfficialRating { get; set; }
public DateTimeOffset DateCreated { get; set; }
public long RunTimeTicks { get; set; }
public List<string> Genres { get; set; }

4
ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs

@ -34,7 +34,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -34,7 +34,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
[Query]
string parentId,
[Query]
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People",
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating",
[Query]
string includeItemTypes = "Movie");
@ -47,7 +47,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -47,7 +47,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
[Query]
string parentId,
[Query]
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People",
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating",
[Query]
string includeItemTypes = "Series");

10
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -276,12 +276,14 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -276,12 +276,14 @@ namespace ErsatzTV.Infrastructure.Jellyfin
var metadata = new MovieMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
Year = item.ProductionYear,
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
DateAdded = dateAdded,
ContentRating = item.OfficialRating,
Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
@ -375,12 +377,14 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -375,12 +377,14 @@ namespace ErsatzTV.Infrastructure.Jellyfin
var metadata = new ShowMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
Year = item.ProductionYear,
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
DateAdded = dateAdded,
ContentRating = item.OfficialRating,
Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
@ -449,6 +453,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -449,6 +453,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
var metadata = new SeasonMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Year = item.ProductionYear,
@ -494,7 +499,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -494,7 +499,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error projecting Jellyfin show");
_logger.LogWarning(ex, "Error projecting Jellyfin season");
return None;
}
}
@ -542,7 +547,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -542,7 +547,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error projecting Jellyfin movie");
_logger.LogWarning(ex, "Error projecting Jellyfin episode");
return None;
}
}
@ -554,6 +559,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin @@ -554,6 +559,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,

1
ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin.Models @@ -9,6 +9,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin.Models
public string Id { get; set; }
public string Etag { get; set; }
public string Path { get; set; }
public string OfficialRating { get; set; }
public DateTimeOffset DateCreated { get; set; }
public long RunTimeTicks { get; set; }
public List<string> Genres { get; set; }

2717
ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.Designer.cs generated

File diff suppressed because it is too large Load Diff

50
ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.cs

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MovieMetadataShowMetadataContentRating : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// local and plex
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 LibraryFolder SET Etag = NULL WHERE LibraryPathId IN
(SELECT LibraryPathId FROM LibraryPath LP
INNER JOIN Library L on LP.LibraryId = L.Id
WHERE L.MediaKind = 1)");
// emby
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL");
// jellyfin
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL");
migrationBuilder.AddColumn<string>(
"ContentRating",
"ShowMetadata",
"TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
"ContentRating",
"MovieMetadata",
"TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
"ContentRating",
"ShowMetadata");
migrationBuilder.DropColumn(
"ContentRating",
"MovieMetadata");
}
}
}

6
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -794,6 +794,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -794,6 +794,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContentRating")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
@ -1170,6 +1173,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1170,6 +1173,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContentRating")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");

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

@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Plex.Models @@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Plex.Models
{
public string Key { get; set; }
public string Title { get; set; }
public string ContentRating { get; set; }
public string Summary { get; set; }
public int Year { get; set; }
public string Tagline { get; set; }

6
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -250,11 +250,13 @@ namespace ErsatzTV.Infrastructure.Plex @@ -250,11 +250,13 @@ namespace ErsatzTV.Infrastructure.Plex
var metadata = new MovieMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,
Year = response.Year,
Tagline = response.Tagline,
ContentRating = response.ContentRating,
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(),
@ -398,11 +400,13 @@ namespace ErsatzTV.Infrastructure.Plex @@ -398,11 +400,13 @@ namespace ErsatzTV.Infrastructure.Plex
var metadata = new ShowMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,
Year = response.Year,
Tagline = response.Tagline,
ContentRating = response.ContentRating,
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(),
@ -462,6 +466,7 @@ namespace ErsatzTV.Infrastructure.Plex @@ -462,6 +466,7 @@ namespace ErsatzTV.Infrastructure.Plex
var metadata = new SeasonMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Year = response.Year,
@ -518,6 +523,7 @@ namespace ErsatzTV.Infrastructure.Plex @@ -518,6 +523,7 @@ namespace ErsatzTV.Infrastructure.Plex
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,

33
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -11,6 +11,9 @@ using ErsatzTV.Core.Interfaces.Search; @@ -11,6 +11,9 @@ using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search;
using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Core;
using Lucene.Net.Analysis.Miscellaneous;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
@ -45,6 +48,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -45,6 +48,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string StyleField = "style";
private const string MoodField = "mood";
private const string ActorField = "actor";
private const string ContentRatingField = "content_rating";
private const string MovieType = "movie";
private const string ShowType = "show";
@ -65,7 +69,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -65,7 +69,7 @@ namespace ErsatzTV.Infrastructure.Search
_initialized = false;
}
public int Version => 9;
public int Version => 10;
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
{
@ -162,9 +166,14 @@ namespace ErsatzTV.Infrastructure.Search @@ -162,9 +166,14 @@ namespace ErsatzTV.Infrastructure.Search
var searcher = new IndexSearcher(reader);
int hitsLimit = limit == 0 ? searcher.IndexReader.MaxDoc : skip + limit;
using var analyzer = new StandardAnalyzer(AppLuceneVersion);
var customAnalyzers = new Dictionary<string, Analyzer>
{
{ ContentRatingField, new KeywordAnalyzer() }
};
using var analyzerWrapper = new PerFieldAnalyzerWrapper(analyzer, customAnalyzers);
QueryParser parser = !string.IsNullOrWhiteSpace(searchField)
? new QueryParser(AppLuceneVersion, searchField, analyzer)
: new MultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzer);
? new QueryParser(AppLuceneVersion, searchField, analyzerWrapper)
: new MultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzerWrapper);
parser.AllowLeadingWildcard = true;
Query query = ParseQuery(searchQuery, parser);
var filter = new DuplicateFilter(TitleAndYearField);
@ -266,6 +275,15 @@ namespace ErsatzTV.Infrastructure.Search @@ -266,6 +275,15 @@ namespace ErsatzTV.Infrastructure.Search
AddLanguages(doc, movie.MediaVersions);
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(
@ -368,6 +386,15 @@ namespace ErsatzTV.Infrastructure.Search @@ -368,6 +386,15 @@ namespace ErsatzTV.Infrastructure.Search
doc.Add(new TextField(LanguageField, cultureInfo.EnglishName, 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(

14
ErsatzTV/Pages/Movie.razor

@ -63,6 +63,18 @@ @@ -63,6 +63,18 @@
</div>
<MudCard Class="mb-6">
<MudCardContent>
@if (_sortedContentRatings.Any())
{
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Content Ratings:&nbsp;</MudText>
<MudLink Href="@($"/search?query=content_rating%3a%22{Uri.EscapeDataString(_sortedContentRatings.Head())}%22")">@_sortedContentRatings.Head()</MudLink>
@foreach (string contentRating in _sortedContentRatings.Skip(1))
{
<MudText>,&nbsp;</MudText>
<MudLink Href="@($"/search?query=content_rating%3a%22{Uri.EscapeDataString(contentRating)}%22")">@contentRating</MudLink>
}
</div>
}
@if (_sortedLanguages.Any())
{
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
@ -136,6 +148,7 @@ @@ -136,6 +148,7 @@
public int MovieId { get; set; }
private MovieViewModel _movie;
private List<string> _sortedContentRatings = new();
private List<CultureInfo> _sortedLanguages = new();
private List<string> _sortedStudios = new();
private List<string> _sortedGenres = new();
@ -147,6 +160,7 @@ @@ -147,6 +160,7 @@
_mediator.Send(new GetMovieById(MovieId)).IfSomeAsync(vm =>
{
_movie = vm;
_sortedContentRatings = _movie.ContentRatings.OrderBy(cr => cr).ToList();
_sortedLanguages = _movie.Languages.OrderBy(ci => ci.EnglishName).ToList();
_sortedStudios = _movie.Studios.OrderBy(s => s).ToList();
_sortedGenres = _movie.Genres.OrderBy(g => g).ToList();

14
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -77,6 +77,18 @@ @@ -77,6 +77,18 @@
</div>
<MudCard Class="mb-6">
<MudCardContent>
@if (_sortedContentRatings.Any())
{
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Content Ratings:&nbsp;</MudText>
<MudLink Href="@($"/search?query=content_rating%3a%22{Uri.EscapeDataString(_sortedContentRatings.Head())}%22")">@_sortedContentRatings.Head()</MudLink>
@foreach (string contentRating in _sortedContentRatings.Skip(1))
{
<MudText>,&nbsp;</MudText>
<MudLink Href="@($"/search?query=content_rating%3a%22{Uri.EscapeDataString(contentRating)}%22")">@contentRating</MudLink>
}
</div>
}
@if (_sortedLanguages.Any())
{
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
@ -161,6 +173,7 @@ @@ -161,6 +173,7 @@
public int ShowId { get; set; }
private TelevisionShowViewModel _show;
private List<string> _sortedContentRatings = new();
private List<CultureInfo> _sortedLanguages = new();
private List<string> _sortedStudios = new();
private List<string> _sortedGenres = new();
@ -179,6 +192,7 @@ @@ -179,6 +192,7 @@
.IfSomeAsync(vm =>
{
_show = vm;
_sortedContentRatings = _show.ContentRatings.OrderBy(cr => cr).ToList();
_sortedLanguages = _show.Languages.OrderBy(ci => ci.EnglishName).ToList();
_sortedStudios = _show.Studios.OrderBy(s => s).ToList();
_sortedGenres = _show.Genres.OrderBy(g => g).ToList();

2
docs/user-guide/search.md

@ -19,6 +19,7 @@ The following fields are available for searching movies: @@ -19,6 +19,7 @@ The following fields are available for searching movies:
- `studio`: The movie studio
- `actor`: An actor from the movie
- `library_name`: The name of the library that contains the movie
- `content_rating`: The movie content rating (case-sensitive)
- `language`: The movie audio stream language
- `release_date`: The movie release date (YYYYMMDD)
- `type`: Always `movie`
@ -34,6 +35,7 @@ The following fields are available for searching shows: @@ -34,6 +35,7 @@ The following fields are available for searching shows:
- `studio`: The show studio
- `actor`: An actor from the show
- `library_name`: The name of the library that contains the show
- `content_rating`: The movie content rating (case-sensitive)
- `language`: The show audio stream language
- `release_date`: The show release date (YYYYMMDD)
- `type`: Always `show`

Loading…
Cancel
Save