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
This commit is contained in:
Jason Dove
2021-05-27 06:41:24 -05:00
committed by GitHub
parent 6504ca10a8
commit 68123a2f9c
34 changed files with 2939 additions and 13 deletions
@@ -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;
@@ -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;
@@ -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);
@@ -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
// 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;
@@ -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
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
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
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
+2 -2
View File
@@ -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
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");
@@ -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; }
@@ -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
[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");
@@ -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
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
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
}
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
}
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
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
@@ -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; }
@@ -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");
}
}
}
@@ -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
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContentRating")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
@@ -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; }
@@ -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
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
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
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,
+30 -3
View File
@@ -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
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
_initialized = false;
}
public int Version => 9;
public int Version => 10;
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
{
@@ -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
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
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(