Browse Source

add movie and show genres (#61)

* load movie genres from sidecar metadata

* search movie and tv show genres

* rebuild all playouts (needed after time zone fix)

* code cleanup

* fix duplicate tv show search results
pull/63/head
Jason Dove 5 years ago committed by GitHub
parent
commit
76d6725dd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      ErsatzTV.Application/MediaCards/Queries/GetSearchCardsHandler.cs
  2. 3
      ErsatzTV.Application/Movies/Mapper.cs
  3. 12
      ErsatzTV.Application/Movies/MovieViewModel.cs
  4. 6
      ErsatzTV.Application/Television/Mapper.cs
  5. 13
      ErsatzTV.Application/Television/TelevisionShowViewModel.cs
  6. 8
      ErsatzTV.Core/Domain/Metadata/Genre.cs
  7. 1
      ErsatzTV.Core/Domain/Metadata/Metadata.cs
  8. 3
      ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs
  9. 37
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  10. 3
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  11. 4
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs
  12. 4
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/ShowMetadataConfiguration.cs
  13. 4
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  14. 32
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  15. 5
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  16. 1560
      ErsatzTV.Infrastructure/Migrations/20210310225515_Add_MetadataGenres.Designer.cs
  17. 75
      ErsatzTV.Infrastructure/Migrations/20210310225515_Add_MetadataGenres.cs
  18. 1560
      ErsatzTV.Infrastructure/Migrations/20210311020345_Reset_MetadataDateUpdated_Genres.Designer.cs
  19. 18
      ErsatzTV.Infrastructure/Migrations/20210311020345_Reset_MetadataDateUpdated_Genres.cs
  20. 1560
      ErsatzTV.Infrastructure/Migrations/20210311022211_RebuildAllPlayouts_TimeZonesAgain.Designer.cs
  21. 18
      ErsatzTV.Infrastructure/Migrations/20210311022211_RebuildAllPlayouts_TimeZonesAgain.cs
  22. 95
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  23. 11
      ErsatzTV/Pages/Movie.razor
  24. 4
      ErsatzTV/Pages/Search.razor
  25. 11
      ErsatzTV/Pages/TelevisionSeasonList.razor

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

@ -18,7 +18,17 @@ namespace ErsatzTV.Application.MediaCards.Queries @@ -18,7 +18,17 @@ namespace ErsatzTV.Application.MediaCards.Queries
public Task<Either<BaseError, SearchCardResultsViewModel>> Handle(
GetSearchCards request,
CancellationToken cancellationToken) =>
Try(_searchRepository.SearchMediaItems(request.Query)).Sequence()
request.Query.StartsWith("genre:")
? GenreSearch(request.Query.Replace("genre:", 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}")));
}

3
ErsatzTV.Application/Movies/Mapper.cs

@ -14,7 +14,8 @@ namespace ErsatzTV.Application.Movies @@ -14,7 +14,8 @@ namespace ErsatzTV.Application.Movies
metadata.Year?.ToString(),
metadata.Plot,
Artwork(metadata, ArtworkKind.Poster),
Artwork(metadata, ArtworkKind.FanArt));
Artwork(metadata, ArtworkKind.FanArt),
metadata.Genres.Map(g => g.Name).ToList());
}
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>

12
ErsatzTV.Application/Movies/MovieViewModel.cs

@ -1,4 +1,12 @@ @@ -1,4 +1,12 @@
namespace ErsatzTV.Application.Movies
using System.Collections.Generic;
namespace ErsatzTV.Application.Movies
{
public record MovieViewModel(string Title, string Year, string Plot, string Poster, string FanArt);
public record MovieViewModel(
string Title,
string Year,
string Plot,
string Poster,
string FanArt,
List<string> Genres);
}

6
ErsatzTV.Application/Television/Mapper.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
@ -13,7 +14,8 @@ namespace ErsatzTV.Application.Television @@ -13,7 +14,8 @@ namespace ErsatzTV.Application.Television
show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(GetFanArt).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>()));
internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
new(

13
ErsatzTV.Application/Television/TelevisionShowViewModel.cs

@ -1,4 +1,13 @@ @@ -1,4 +1,13 @@
namespace ErsatzTV.Application.Television
using System.Collections.Generic;
namespace ErsatzTV.Application.Television
{
public record TelevisionShowViewModel(int Id, string Title, string Year, string Plot, string Poster, string FanArt);
public record TelevisionShowViewModel(
int Id,
string Title,
string Year,
string Plot,
string Poster,
string FanArt,
List<string> Genres);
}

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

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

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

@ -15,5 +15,6 @@ namespace ErsatzTV.Core.Domain @@ -15,5 +15,6 @@ namespace ErsatzTV.Core.Domain
public DateTime DateAdded { get; set; }
public DateTime DateUpdated { get; set; }
public List<Artwork> Artwork { get; set; }
public List<Genre> Genres { get; set; }
}
}

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

@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{
public interface ISearchRepository
{
public Task<List<MediaItem>> SearchMediaItems(string query);
public Task<List<MediaItem>> SearchMediaItemsByTitle(string query);
public Task<List<MediaItem>> SearchMediaItemsByGenre(string genre);
}
}

37
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ErsatzTV.Core.Domain;
@ -133,6 +134,18 @@ namespace ErsatzTV.Core.Metadata @@ -133,6 +134,18 @@ namespace ErsatzTV.Core.Metadata
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle;
foreach (Genre genre in existing.Genres.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Remove(genre);
}
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
}
},
() =>
{
@ -163,6 +176,18 @@ namespace ErsatzTV.Core.Metadata @@ -163,6 +176,18 @@ namespace ErsatzTV.Core.Metadata
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle;
foreach (Genre genre in existing.Genres.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Remove(genre);
}
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
}
},
() =>
{
@ -224,7 +249,8 @@ namespace ErsatzTV.Core.Metadata @@ -224,7 +249,8 @@ namespace ErsatzTV.Core.Metadata
Outline = nfo.Outline,
Tagline = nfo.Tagline,
Year = nfo.Year,
ReleaseDate = GetAired(nfo.Premiered) ?? new DateTime(nfo.Year, 1, 1)
ReleaseDate = GetAired(nfo.Premiered) ?? new DateTime(nfo.Year, 1, 1),
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList()
},
None);
}
@ -279,7 +305,8 @@ namespace ErsatzTV.Core.Metadata @@ -279,7 +305,8 @@ namespace ErsatzTV.Core.Metadata
ReleaseDate = nfo.Premiered,
Plot = nfo.Plot,
Outline = nfo.Outline,
Tagline = nfo.Tagline
Tagline = nfo.Tagline,
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList()
},
None);
}
@ -328,6 +355,9 @@ namespace ErsatzTV.Core.Metadata @@ -328,6 +355,9 @@ namespace ErsatzTV.Core.Metadata
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
}
[XmlRoot("tvshow")]
@ -350,6 +380,9 @@ namespace ErsatzTV.Core.Metadata @@ -350,6 +380,9 @@ namespace ErsatzTV.Core.Metadata
[XmlElement("premiered")]
public string Premiered { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
}
[XmlRoot("episodedetails")]

3
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -297,7 +297,8 @@ namespace ErsatzTV.Core.Scheduling @@ -297,7 +297,8 @@ namespace ErsatzTV.Core.Scheduling
{
NextScheduleItem = schedule,
NextScheduleItemId = schedule.Id,
NextStart = (start - start.TimeOfDay).UtcDateTime + schedule.StartTime.GetValueOrDefault()
NextStart = (start - start.TimeOfDay).UtcDateTime +
schedule.StartTime.GetValueOrDefault()
};
case StartType.Dynamic:
default:

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

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

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

@ -13,6 +13,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -13,6 +13,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.HasMany(sm => sm.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Genres)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}

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

@ -35,6 +35,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -35,6 +35,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await dbContext.Movies
.Include(m => m.MovieMetadata)
.ThenInclude(m => m.Artwork)
.Include(m => m.MovieMetadata)
.ThenInclude(m => m.Genres)
.OrderBy(m => m.Id)
.SingleOrDefaultAsync(m => m.Id == movieId)
.Map(Optional);
@ -45,6 +47,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -45,6 +47,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Option<Movie> maybeExisting = await _dbContext.Movies
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Genres)
.Include(i => i.LibraryPath)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)

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

@ -21,7 +21,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -21,7 +21,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
_dbConnection = dbConnection;
}
public async Task<List<MediaItem>> SearchMediaItems(string query)
public async Task<List<MediaItem>> SearchMediaItemsByTitle(string query)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT M.Id FROM Movie M
@ -30,7 +30,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -30,7 +30,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
UNION
SELECT S.Id FROM Show S
INNER JOIN ShowMetadata SM on S.Id = SM.ShowId
WHERE SM.Title LIKE @Query",
WHERE SM.Title LIKE @Query
GROUP BY SM.Title, SM.Year",
new { Query = $"%{query}%" })
.Map(results => results.ToList());
@ -44,5 +45,32 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -44,5 +45,32 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.OfType<MediaItem>()
.ToListAsync();
}
public async Task<List<MediaItem>> SearchMediaItemsByGenre(string genre)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT M.Id FROM Movie M
INNER JOIN MovieMetadata MM on M.Id = MM.MovieId
INNER JOIN Genre G on MM.Id = G.MovieMetadataId
WHERE G.Name LIKE @Query
UNION
SELECT S.Id FROM Show S
INNER JOIN ShowMetadata SM on S.Id = SM.ShowId
INNER JOIN Genre G2 on SM.Id = G2.ShowMetadataId
WHERE G2.Name LIKE @Query
GROUP BY SM.Title, SM.Year",
new { Query = genre })
.Map(results => results.ToList());
await using TvContext context = _dbContextFactory.CreateDbContext();
return await context.MediaItems
.Filter(m => ids.Contains(m.Id))
.Include(m => (m as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => (m as Show).ShowMetadata)
.ThenInclude(mm => mm.Artwork)
.OfType<MediaItem>()
.ToListAsync();
}
}
}

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

@ -55,6 +55,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -55,6 +55,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Filter(s => s.Id == showId)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Genres)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync()
.Map(Optional);
@ -171,6 +173,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -171,6 +173,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return _dbContext.Shows
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Genres)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == id)
.Map(Optional);
@ -183,6 +187,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -183,6 +187,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
try
{
metadata.DateAdded = DateTime.UtcNow;
metadata.Genres ??= new List<Genre>();
var show = new Show
{
LibraryPathId = libraryPathId,

1560
ErsatzTV.Infrastructure/Migrations/20210310225515_Add_MetadataGenres.Designer.cs generated

File diff suppressed because it is too large Load Diff

75
ErsatzTV.Infrastructure/Migrations/20210310225515_Add_MetadataGenres.cs

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MetadataGenres : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
"Genre",
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_Genre", x => x.Id);
table.ForeignKey(
"FK_Genre_EpisodeMetadata_EpisodeMetadataId",
x => x.EpisodeMetadataId,
"EpisodeMetadata",
"Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
"FK_Genre_MovieMetadata_MovieMetadataId",
x => x.MovieMetadataId,
"MovieMetadata",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_Genre_SeasonMetadata_SeasonMetadataId",
x => x.SeasonMetadataId,
"SeasonMetadata",
"Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
"FK_Genre_ShowMetadata_ShowMetadataId",
x => x.ShowMetadataId,
"ShowMetadata",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
"IX_Genre_EpisodeMetadataId",
"Genre",
"EpisodeMetadataId");
migrationBuilder.CreateIndex(
"IX_Genre_MovieMetadataId",
"Genre",
"MovieMetadataId");
migrationBuilder.CreateIndex(
"IX_Genre_SeasonMetadataId",
"Genre",
"SeasonMetadataId");
migrationBuilder.CreateIndex(
"IX_Genre_ShowMetadataId",
"Genre",
"ShowMetadataId");
}
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropTable(
"Genre");
}
}

1560
ErsatzTV.Infrastructure/Migrations/20210311020345_Reset_MetadataDateUpdated_Genres.Designer.cs generated

File diff suppressed because it is too large Load Diff

18
ErsatzTV.Infrastructure/Migrations/20210311020345_Reset_MetadataDateUpdated_Genres.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Reset_MetadataDateUpdated_Genres : 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)
{
}
}
}

1560
ErsatzTV.Infrastructure/Migrations/20210311022211_RebuildAllPlayouts_TimeZonesAgain.Designer.cs generated

File diff suppressed because it is too large Load Diff

18
ErsatzTV.Infrastructure/Migrations/20210311022211_RebuildAllPlayouts_TimeZonesAgain.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class RebuildAllPlayouts_TimeZonesAgain : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DELETE FROM PlayoutItem");
migrationBuilder.Sql(@"DELETE FROM PlayoutProgramScheduleAnchor");
migrationBuilder.Sql(@"UPDATE Playout SET Anchor_NextStart = null, Anchor_NextScheduleItemId = null");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

95
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -274,6 +274,42 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -274,6 +274,42 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("FFmpegProfile");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Genre",
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("Genre");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Library",
b =>
@ -1068,6 +1104,29 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1068,6 +1104,29 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Resolution");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Genre",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
.WithMany("Genres")
.HasForeignKey("EpisodeMetadataId");
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
.WithMany("Genres")
.HasForeignKey("MovieMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
.WithMany("Genres")
.HasForeignKey("SeasonMetadataId");
b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null)
.WithMany("Genres")
.HasForeignKey("ShowMetadataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Library",
b =>
@ -1543,7 +1602,14 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1543,7 +1602,14 @@ namespace ErsatzTV.Infrastructure.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => { b.Navigation("CollectionItems"); });
modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => { b.Navigation("Artwork"); });
modelBuilder.Entity(
"ErsatzTV.Core.Domain.EpisodeMetadata",
b =>
{
b.Navigation("Artwork");
b.Navigation("Genres");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => { b.Navigation("Paths"); });
@ -1555,7 +1621,14 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1555,7 +1621,14 @@ namespace ErsatzTV.Infrastructure.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => { b.Navigation("MediaFiles"); });
modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => { b.Navigation("Artwork"); });
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MovieMetadata",
b =>
{
b.Navigation("Artwork");
b.Navigation("Genres");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Playout",
@ -1575,9 +1648,23 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1575,9 +1648,23 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => { b.Navigation("Artwork"); });
modelBuilder.Entity(
"ErsatzTV.Core.Domain.SeasonMetadata",
b =>
{
b.Navigation("Artwork");
modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => { b.Navigation("Artwork"); });
b.Navigation("Genres");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ShowMetadata",
b =>
{
b.Navigation("Artwork");
b.Navigation("Genres");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Episode",

11
ErsatzTV/Pages/Movie.razor

@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
<div style="display: flex; flex-direction: row;">
<div style="display: flex; flex-direction: row;" class="mb-6">
@if (!string.IsNullOrWhiteSpace(_movie.Poster))
{
<img class="mud-elevation-2 mr-6"
@ -43,6 +43,15 @@ @@ -43,6 +43,15 @@
</div>
</div>
</div>
@if (_movie.Genres.Any())
{
<div>
@foreach (string genre in _movie.Genres.OrderBy(g => g))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@genre" Class="mr-2" Link="@($"/search?query=genre%3a{genre}")"/>
}
</div>
}
</MudContainer>
@code {

4
ErsatzTV/Pages/Search.razor

@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
<MudText GutterBottom="true" Typo="Typo.h4">Movies</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MovieCardViewModel card in _data.MovieCards)
@foreach (MovieCardViewModel card in _data.MovieCards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Link="@($"/media/movies/{card.MovieId}")"
@ -37,7 +37,7 @@ @@ -37,7 +37,7 @@
<MudText GutterBottom="true" Typo="Typo.h4">Television Shows</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionShowCardViewModel card in _data.ShowCards)
@foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Link="@($"/media/tv/shows/{card.TelevisionShowId}")"

11
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
<div style="display: flex; flex-direction: row;">
<div style="display: flex; flex-direction: row;" class="mb-6">
@if (!string.IsNullOrWhiteSpace(_show.Poster))
{
<img class="mud-elevation-2 mr-6"
@ -58,6 +58,15 @@ @@ -58,6 +58,15 @@
</div>
</div>
</div>
@if (_show.Genres.Any())
{
<div>
@foreach (string genre in _show.Genres.OrderBy(g => g))
{
<MudFab Color="Color.Info" Size="Size.Small" Label="@genre" Class="mr-2" Link="@($"/search?query=genre%3a{genre}")"/>
}
</div>
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid mt-8">
@foreach (TelevisionSeasonCardViewModel card in _data.Cards)

Loading…
Cancel
Save