From 95eb1bdd6f46cb08bba22ac1176982380def9a6a Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sat, 13 Feb 2021 13:58:38 -0600 Subject: [PATCH] support movie posters --- .../MediaItems/AggregateMediaItemViewModel.cs | 4 +- .../Commands/CreateMediaItemHandler.cs | 6 +- .../Commands/RefreshMediaItemPoster.cs | 9 + .../Queries/GetAggregateMediaItemsHandler.cs | 8 +- .../MediaItems/Queries/GetPosterContents.cs | 9 + .../Queries/GetPosterContentsHandler.cs | 71 ++ .../AggregateModels/MediaItemSummary.cs | 2 +- ErsatzTV.Core/Domain/MediaItem.cs | 1 + .../Metadata/ILocalPosterProvider.cs | 10 + ErsatzTV.Core/Metadata/LocalMediaScanner.cs | 4 + ErsatzTV.Core/Metadata/LocalPosterProvider.cs | 54 ++ .../GenericIntegerIdConfiguration.cs | 2 +- .../MediaCollectionSummaryConfiguration.cs | 2 +- .../MediaItemSummaryConfiguration.cs | 2 +- .../Data/Repositories/MediaItemRepository.cs | 14 +- ErsatzTV.Infrastructure/Data/TvContext.cs | 5 + .../20210213155419_MetadataSortTitle.cs | 14 +- ...0213191309_MediaItemPosterPath.Designer.cs | 857 ++++++++++++++++++ .../20210213191309_MediaItemPosterPath.cs | 48 + .../Migrations/TvContextModelSnapshot.cs | 32 +- ErsatzTV/Controllers/PostersController.cs | 28 + ErsatzTV/Services/WorkerService.cs | 1 + ErsatzTV/Shared/MediaCard.razor | 18 +- ErsatzTV/Shared/MediaItemsGrid.razor | 4 +- ErsatzTV/Startup.cs | 1 + ErsatzTV/wwwroot/css/site.css | 8 +- 26 files changed, 1155 insertions(+), 59 deletions(-) create mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs create mode 100644 ErsatzTV.Application/MediaItems/Queries/GetPosterContents.cs create mode 100644 ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs create mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs create mode 100644 ErsatzTV.Core/Metadata/LocalPosterProvider.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.cs create mode 100644 ErsatzTV/Controllers/PostersController.cs diff --git a/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs b/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs index c884fa37a..a6005b44f 100644 --- a/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs +++ b/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs @@ -1,7 +1,9 @@ namespace ErsatzTV.Application.MediaItems { public record AggregateMediaItemViewModel( + int MediaItemId, string Title, string Subtitle, - string SortTitle) : IMediaCard; + string SortTitle, + bool HasPoster); } diff --git a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs b/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs index 5002cfc7c..53509737f 100644 --- a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs +++ b/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs @@ -16,6 +16,7 @@ namespace ErsatzTV.Application.MediaItems.Commands { private readonly IConfigElementRepository _configElementRepository; private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly ILocalPosterProvider _localPosterProvider; private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaSourceRepository _mediaSourceRepository; @@ -27,7 +28,8 @@ namespace ErsatzTV.Application.MediaItems.Commands IConfigElementRepository configElementRepository, ISmartCollectionBuilder smartCollectionBuilder, ILocalMetadataProvider localMetadataProvider, - ILocalStatisticsProvider localStatisticsProvider) + ILocalStatisticsProvider localStatisticsProvider, + ILocalPosterProvider localPosterProvider) { _mediaItemRepository = mediaItemRepository; _mediaSourceRepository = mediaSourceRepository; @@ -35,6 +37,7 @@ namespace ErsatzTV.Application.MediaItems.Commands _smartCollectionBuilder = smartCollectionBuilder; _localMetadataProvider = localMetadataProvider; _localStatisticsProvider = localStatisticsProvider; + _localPosterProvider = localPosterProvider; } public Task> Handle( @@ -50,6 +53,7 @@ namespace ErsatzTV.Application.MediaItems.Commands await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem); await _localMetadataProvider.RefreshMetadata(parameters.MediaItem); + await _localPosterProvider.RefreshPoster(parameters.MediaItem); await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem); return ProjectToViewModel(parameters.MediaItem); diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs new file mode 100644 index 000000000..c6d7ddb85 --- /dev/null +++ b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Application.MediaItems.Commands +{ + public record RefreshMediaItemPoster : RefreshMediaItem + { + public RefreshMediaItemPoster(int mediaItemId) : base(mediaItemId) + { + } + } +} diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs index b9bc8aa8d..5b2edda53 100644 --- a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs +++ b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs @@ -28,7 +28,13 @@ namespace ErsatzTV.Application.MediaItems.Queries request.PageSize); var results = allItems - .Map(s => new AggregateMediaItemViewModel(s.Title, s.Subtitle, s.SortTitle)) + .Map( + s => new AggregateMediaItemViewModel( + s.MediaItemId, + s.Title, + s.Subtitle, + s.SortTitle, + !string.IsNullOrWhiteSpace(s.PosterPath))) .ToList(); return new AggregateMediaItemResults(count, results); diff --git a/ErsatzTV.Application/MediaItems/Queries/GetPosterContents.cs b/ErsatzTV.Application/MediaItems/Queries/GetPosterContents.cs new file mode 100644 index 000000000..08071df5e --- /dev/null +++ b/ErsatzTV.Application/MediaItems/Queries/GetPosterContents.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Application.Images; +using ErsatzTV.Core; +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.MediaItems.Queries +{ + public record GetPosterContents(int MediaItemId) : IRequest>; +} diff --git a/ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs new file mode 100644 index 000000000..d39797c45 --- /dev/null +++ b/ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Application.Images; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using MediatR; +using Microsoft.Extensions.Caching.Memory; +using Winista.Mime; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Application.MediaItems.Queries +{ + public class GetPosterContentsHandler : IRequestHandler> + { + private static readonly MimeTypes MimeTypes = new(); + + private readonly IMediaItemRepository _mediaItemRepository; + private readonly IMemoryCache _memoryCache; + + public GetPosterContentsHandler(IMediaItemRepository mediaItemRepository, IMemoryCache memoryCache) + { + _mediaItemRepository = mediaItemRepository; + _memoryCache = memoryCache; + } + + public Task> Handle( + GetPosterContents request, + CancellationToken cancellationToken) => + Validate(request) + .Map(v => v.ToEither()) + .BindT(GetPoster); + + private async Task> GetPoster(MediaItem mediaItem) + { + try + { + return await _memoryCache.GetOrCreateAsync( + mediaItem.PosterPath, + async entry => + { + entry.SlidingExpiration = TimeSpan.FromHours(1); + + byte[] contents = await File.ReadAllBytesAsync(mediaItem.PosterPath); + MimeType mimeType = MimeTypes.GetMimeType(contents); + return new ImageViewModel(contents, mimeType.Name); + }); + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private Task> Validate(GetPosterContents request) => + MediaItemMustExist(request).BindT(PosterPathMustExist); + + private async Task> MediaItemMustExist(GetPosterContents request) => + (await _mediaItemRepository.Get(request.MediaItemId)) + .ToValidation($"MediaItem {request.MediaItemId} does not exist."); + + private static Validation PosterPathMustExist(MediaItem mediaItem) => + Optional(mediaItem.PosterPath) + .Filter(p => !string.IsNullOrWhiteSpace(p) && File.Exists(p)) + .Map(_ => mediaItem) + .ToValidation($"MediaItem {mediaItem.Id} does not have a poster"); + } +} diff --git a/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs b/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs index 7d8b9174f..0ae3f6920 100644 --- a/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs +++ b/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs @@ -1,4 +1,4 @@ namespace ErsatzTV.Core.AggregateModels { - public record MediaItemSummary(string Title, string SortTitle, string Subtitle); + public record MediaItemSummary(string Title, string SortTitle, string Subtitle, string PosterPath, int MediaItemId); } diff --git a/ErsatzTV.Core/Domain/MediaItem.cs b/ErsatzTV.Core/Domain/MediaItem.cs index 39adcc026..87a6f7d48 100644 --- a/ErsatzTV.Core/Domain/MediaItem.cs +++ b/ErsatzTV.Core/Domain/MediaItem.cs @@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Domain public int MediaSourceId { get; set; } public MediaSource Source { get; set; } public string Path { get; set; } + public string PosterPath { get; set; } public MediaMetadata Metadata { get; set; } public DateTime? LastWriteTime { get; set; } public IList SimpleMediaCollections { get; set; } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs new file mode 100644 index 000000000..ab47e5da0 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Interfaces.Metadata +{ + public interface ILocalPosterProvider + { + Task RefreshPoster(MediaItem mediaItem); + } +} diff --git a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs b/ErsatzTV.Core/Metadata/LocalMediaScanner.cs index 54cf57a9c..82def5ae3 100644 --- a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalMediaScanner.cs @@ -16,6 +16,7 @@ namespace ErsatzTV.Core.Metadata public class LocalMediaScanner : ILocalMediaScanner { private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly ILocalPosterProvider _localPosterProvider; private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; @@ -28,6 +29,7 @@ namespace ErsatzTV.Core.Metadata IPlayoutRepository playoutRepository, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, + ILocalPosterProvider localPosterProvider, ISmartCollectionBuilder smartCollectionBuilder, IPlayoutBuilder playoutBuilder, ILogger logger) @@ -36,6 +38,7 @@ namespace ErsatzTV.Core.Metadata _playoutRepository = playoutRepository; _localStatisticsProvider = localStatisticsProvider; _localMetadataProvider = localMetadataProvider; + _localPosterProvider = localPosterProvider; _smartCollectionBuilder = smartCollectionBuilder; _playoutBuilder = playoutBuilder; _logger = logger; @@ -135,6 +138,7 @@ namespace ErsatzTV.Core.Metadata { await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem); await _localMetadataProvider.RefreshMetadata(mediaItem); + await _localPosterProvider.RefreshPoster(mediaItem); await _smartCollectionBuilder.RefreshSmartCollections(mediaItem); } } diff --git a/ErsatzTV.Core/Metadata/LocalPosterProvider.cs b/ErsatzTV.Core/Metadata/LocalPosterProvider.cs new file mode 100644 index 000000000..ae483bc5a --- /dev/null +++ b/ErsatzTV.Core/Metadata/LocalPosterProvider.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Core.Metadata +{ + public class LocalPosterProvider : ILocalPosterProvider + { + private readonly IMediaItemRepository _mediaItemRepository; + + public LocalPosterProvider(IMediaItemRepository mediaItemRepository) => + _mediaItemRepository = mediaItemRepository; + + public Task RefreshPoster(MediaItem mediaItem) + { + Option maybePoster = mediaItem.Metadata.MediaType switch + { + MediaType.Movie => RefreshMoviePoster(mediaItem), + MediaType.TvShow => RefreshTelevisionPoster(mediaItem), + _ => None + }; + + return maybePoster.Match( + path => + { + mediaItem.PosterPath = path; + return _mediaItemRepository.Update(mediaItem); + }, + Task.CompletedTask); + } + + private static Option RefreshMoviePoster(MediaItem mediaItem) + { + string folder = Path.GetDirectoryName(mediaItem.Path); + if (folder != null) + { + string[] possiblePaths = + { "poster.jpg", Path.GetFileNameWithoutExtension(mediaItem.Path) + "-poster.jpg" }; + Option maybePoster = + possiblePaths.Map(p => Path.Combine(folder, p)).FirstOrDefault(File.Exists); + return maybePoster; + } + + return None; + } + + private Option RefreshTelevisionPoster(MediaItem mediaItem) => None; + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs index f5842dc06..c2c793c40 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations public class GenericIntegerIdConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey(); + builder.HasNoKey().ToView(null); } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs index 17a64ae0e..66187d03c 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations public class MediaCollectionSummaryConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey(); + builder.HasNoKey().ToView(null); } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs index cd7c9ef09..58cb49c43 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations public class MediaItemSummaryConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey(); + builder.HasNoKey().ToView(null); } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs index 943e50880..638cdecf2 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs @@ -45,7 +45,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories mediaType switch { MediaType.Movie => _dbContext.MediaItemSummaries.FromSqlRaw( - @"SELECT Metadata_Title AS Title, Metadata_SortTitle AS SortTitle, substr(Metadata_Aired, 1, 4) AS Subtitle + @"SELECT + Metadata_Title AS Title, + Metadata_SortTitle AS SortTitle, + substr(Metadata_Aired, 1, 4) AS Subtitle, + PosterPath, + Id as MediaItemId FROM MediaItems WHERE Metadata_MediaType=2 ORDER BY Metadata_SortTitle LIMIT {0} OFFSET {1}", @@ -54,7 +59,12 @@ LIMIT {0} OFFSET {1}", .AsNoTracking() .ToListAsync(), MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw( - @"SELECT Metadata_Title AS Title, Metadata_SortTitle AS SortTitle, count(*) || ' Episodes' AS Subtitle + @"SELECT + Metadata_Title AS Title, + Metadata_SortTitle AS SortTitle, + count(*) || ' Episodes' AS Subtitle, + PosterPath, + min(Id) as MediaItemId FROM MediaItems WHERE Metadata_MediaType=1 GROUP BY Metadata_Title, Metadata_SortTitle ORDER BY Metadata_SortTitle diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index e3471a62b..6e881c608 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -40,6 +40,11 @@ namespace ErsatzTV.Infrastructure.Data protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + + builder.Ignore(); + builder.Ignore(); + builder.Ignore(); + builder.ApplyConfigurationsFromAssembly(typeof(TvContext).Assembly); } } diff --git a/ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs b/ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs index 0eab76ea6..112d422f8 100644 --- a/ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs +++ b/ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs @@ -7,9 +7,9 @@ namespace ErsatzTV.Infrastructure.Migrations protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( - name: "Metadata_SortTitle", - table: "MediaItems", - type: "TEXT", + "Metadata_SortTitle", + "MediaItems", + "TEXT", nullable: true); migrationBuilder.Sql( @@ -22,11 +22,9 @@ SET Metadata_SortTitle = substr(Metadata_Title, 5) WHERE Metadata_Title LIKE 'the %'"); } - protected override void Down(MigrationBuilder migrationBuilder) - { + protected override void Down(MigrationBuilder migrationBuilder) => migrationBuilder.DropColumn( - name: "Metadata_SortTitle", - table: "MediaItems"); - } + "Metadata_SortTitle", + "MediaItems"); } } diff --git a/ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.Designer.cs new file mode 100644 index 000000000..12294fe3c --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.Designer.cs @@ -0,0 +1,857 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210213191309_MediaItemPosterPath")] + partial class MediaItemPosterPath + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.3"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("AudioVolume") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeAudioCodec") + .HasColumnType("INTEGER"); + + b.Property("NormalizeResolution") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideoCodec") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfiles"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PosterPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleItemAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceConnections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceLibraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedules"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolutions"); + }); + + modelBuilder.Entity("MediaItemSimpleMediaCollection", b => + { + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.HasKey("ItemsId", "SimpleMediaCollectionsId"); + + b.HasIndex("SimpleMediaCollectionsId"); + + b.ToTable("MediaItemSimpleMediaCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + + b.ToTable("SimpleMediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowTitle") + .HasColumnType("TEXT"); + + b.HasIndex("ShowTitle", "SeasonNumber") + .IsUnique(); + + b.ToTable("TelevisionMediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("Folder") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.ToTable("LocalMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaMetadata", "Metadata", b1 => + { + b1.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b1.Property("Aired") + .HasColumnType("TEXT"); + + b1.Property("AudioCodec") + .HasColumnType("TEXT"); + + b1.Property("ContentRating") + .HasColumnType("TEXT"); + + b1.Property("Description") + .HasColumnType("TEXT"); + + b1.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("Duration") + .HasColumnType("TEXT"); + + b1.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b1.Property("Height") + .HasColumnType("INTEGER"); + + b1.Property("MediaType") + .HasColumnType("INTEGER"); + + b1.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b1.Property("SortTitle") + .HasColumnType("TEXT"); + + b1.Property("Subtitle") + .HasColumnType("TEXT"); + + b1.Property("Title") + .HasColumnType("TEXT"); + + b1.Property("VideoCodec") + .HasColumnType("TEXT"); + + b1.Property("VideoScanType") + .HasColumnType("INTEGER"); + + b1.Property("Width") + .HasColumnType("INTEGER"); + + b1.HasKey("MediaItemId"); + + b1.ToTable("MediaItems"); + + b1.WithOwner() + .HasForeignKey("MediaItemId"); + }); + + b.Navigation("Metadata"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playouts"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaCollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorProgramScheduleId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorMediaCollectionId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + + b1.ToTable("PlayoutProgramScheduleItemAnchors"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaCollection"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Libraries") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaCollection"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("MediaItemSimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("Libraries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.cs b/ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.cs new file mode 100644 index 000000000..665df2064 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class MediaItemPosterPath : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "GenericIntegerIds"); + + migrationBuilder.DropTable( + "MediaCollectionSummaries"); + + migrationBuilder.AddColumn( + "PosterPath", + "MediaItems", + "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + "PosterPath", + "MediaItems"); + + migrationBuilder.CreateTable( + "GenericIntegerIds", + table => new + { + Id = table.Column("INTEGER", nullable: false) + }, + constraints: table => { }); + + migrationBuilder.CreateTable( + "MediaCollectionSummaries", + table => new + { + Id = table.Column("INTEGER", nullable: false), + IsSimple = table.Column("INTEGER", nullable: false), + ItemCount = table.Column("INTEGER", nullable: false), + Name = table.Column("TEXT", nullable: true) + }, + constraints: table => { }); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 7de29978c..a22b6828b 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -16,35 +16,6 @@ namespace ErsatzTV.Infrastructure.Migrations modelBuilder .HasAnnotation("ProductVersion", "5.0.3"); - modelBuilder.Entity( - "ErsatzTV.Core.AggregateModels.GenericIntegerId", - b => - { - b.Property("Id") - .HasColumnType("INTEGER"); - - b.ToTable("GenericIntegerIds"); - }); - - modelBuilder.Entity( - "ErsatzTV.Core.AggregateModels.MediaCollectionSummary", - b => - { - b.Property("Id") - .HasColumnType("INTEGER"); - - b.Property("IsSimple") - .HasColumnType("INTEGER"); - - b.Property("ItemCount") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.ToTable("MediaCollectionSummaries"); - }); - modelBuilder.Entity( "ErsatzTV.Core.Domain.Channel", b => @@ -205,6 +176,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("Path") .HasColumnType("TEXT"); + b.Property("PosterPath") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.HasIndex("MediaSourceId"); diff --git a/ErsatzTV/Controllers/PostersController.cs b/ErsatzTV/Controllers/PostersController.cs new file mode 100644 index 000000000..518503e7b --- /dev/null +++ b/ErsatzTV/Controllers/PostersController.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using ErsatzTV.Application.Images; +using ErsatzTV.Application.MediaItems.Queries; +using ErsatzTV.Core; +using LanguageExt; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace ErsatzTV.Controllers +{ + [ApiController] + [ApiExplorerSettings(IgnoreApi = true)] + public class PostersController : ControllerBase + { + private readonly IMediator _mediator; + + public PostersController(IMediator mediator) => _mediator = mediator; + + [HttpGet("/posters/{mediaItemId}")] + public async Task ForMediaItem(int mediaItemId) + { + Either imageContents = await _mediator.Send(new GetPosterContents(mediaItemId)); + return imageContents.Match( + Left: _ => new NotFoundResult(), + Right: r => new FileContentResult(r.Contents, r.MimeType)); + } + } +} diff --git a/ErsatzTV/Services/WorkerService.cs b/ErsatzTV/Services/WorkerService.cs index eb7f3fe31..a7f1bbb19 100644 --- a/ErsatzTV/Services/WorkerService.cs +++ b/ErsatzTV/Services/WorkerService.cs @@ -62,6 +62,7 @@ namespace ErsatzTV.Services RefreshMediaItemMetadata => "metadata", RefreshMediaItemStatistics => "statistics", RefreshMediaItemCollections => "collections", + RefreshMediaItemPoster => "poster", _ => "" }; diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 224f74d97..e219cce2f 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -1,8 +1,16 @@ -
+@using ErsatzTV.Application.MediaItems +
- - @Placeholder(Data.SortTitle) - + @if (Data.HasPoster) + { + + } + else + { + + @Placeholder(Data.SortTitle) + + } @Data.Title @@ -15,7 +23,7 @@ @code { [Parameter] - public IMediaCard Data { get; set; } + public AggregateMediaItemViewModel Data { get; set; } private string Placeholder(string sortTitle) { diff --git a/ErsatzTV/Shared/MediaItemsGrid.razor b/ErsatzTV/Shared/MediaItemsGrid.razor index 09e355bbe..fb9882c03 100644 --- a/ErsatzTV/Shared/MediaItemsGrid.razor +++ b/ErsatzTV/Shared/MediaItemsGrid.razor @@ -3,7 +3,7 @@ @inject IMediator Mediator - + @@ -45,7 +45,7 @@ _pageNumber -= 1; await RefreshData(); } - + private async Task NextPage() { _pageNumber += 1; diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index e4403bdbd..911715366 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -164,6 +164,7 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index e91d6bf70..b612390cd 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -4,9 +4,7 @@ flex-wrap: wrap; } -.media-card-container { - width: 152px; -} +.media-card-container { width: 152px; } .media-card { display: flex; @@ -23,6 +21,4 @@ width: 100%; } -.media-card-poster-placeholder { - font-weight: bold; -} \ No newline at end of file +.media-card-poster-placeholder { font-weight: bold; } \ No newline at end of file