Browse Source

support movie posters

pull/22/head
Jason Dove 6 years ago
parent
commit
95eb1bdd6f
  1. 4
      ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs
  2. 6
      ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs
  3. 9
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs
  4. 8
      ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs
  5. 9
      ErsatzTV.Application/MediaItems/Queries/GetPosterContents.cs
  6. 71
      ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs
  7. 2
      ErsatzTV.Core/AggregateModels/MediaItemSummary.cs
  8. 1
      ErsatzTV.Core/Domain/MediaItem.cs
  9. 10
      ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs
  10. 4
      ErsatzTV.Core/Metadata/LocalMediaScanner.cs
  11. 54
      ErsatzTV.Core/Metadata/LocalPosterProvider.cs
  12. 2
      ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs
  13. 2
      ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs
  14. 2
      ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs
  15. 14
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  16. 5
      ErsatzTV.Infrastructure/Data/TvContext.cs
  17. 14
      ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs
  18. 857
      ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.Designer.cs
  19. 48
      ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.cs
  20. 32
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  21. 28
      ErsatzTV/Controllers/PostersController.cs
  22. 1
      ErsatzTV/Services/WorkerService.cs
  23. 18
      ErsatzTV/Shared/MediaCard.razor
  24. 4
      ErsatzTV/Shared/MediaItemsGrid.razor
  25. 1
      ErsatzTV/Startup.cs
  26. 8
      ErsatzTV/wwwroot/css/site.css

4
ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs

@ -1,7 +1,9 @@ @@ -1,7 +1,9 @@
namespace ErsatzTV.Application.MediaItems
{
public record AggregateMediaItemViewModel(
int MediaItemId,
string Title,
string Subtitle,
string SortTitle) : IMediaCard;
string SortTitle,
bool HasPoster);
}

6
ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs

@ -16,6 +16,7 @@ namespace ErsatzTV.Application.MediaItems.Commands @@ -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 @@ -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 @@ -35,6 +37,7 @@ namespace ErsatzTV.Application.MediaItems.Commands
_smartCollectionBuilder = smartCollectionBuilder;
_localMetadataProvider = localMetadataProvider;
_localStatisticsProvider = localStatisticsProvider;
_localPosterProvider = localPosterProvider;
}
public Task<Either<BaseError, MediaItemViewModel>> Handle(
@ -50,6 +53,7 @@ namespace ErsatzTV.Application.MediaItems.Commands @@ -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);

9
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemPoster : RefreshMediaItem
{
public RefreshMediaItemPoster(int mediaItemId) : base(mediaItemId)
{
}
}
}

8
ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs

@ -28,7 +28,13 @@ namespace ErsatzTV.Application.MediaItems.Queries @@ -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);

9
ErsatzTV.Application/MediaItems/Queries/GetPosterContents.cs

@ -0,0 +1,9 @@ @@ -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<Either<BaseError, ImageViewModel>>;
}

71
ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs

@ -0,0 +1,71 @@ @@ -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<GetPosterContents, Either<BaseError, ImageViewModel>>
{
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<Either<BaseError, ImageViewModel>> Handle(
GetPosterContents request,
CancellationToken cancellationToken) =>
Validate(request)
.Map(v => v.ToEither<MediaItem>())
.BindT(GetPoster);
private async Task<Either<BaseError, ImageViewModel>> 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<Validation<BaseError, MediaItem>> Validate(GetPosterContents request) =>
MediaItemMustExist(request).BindT(PosterPathMustExist);
private async Task<Validation<BaseError, MediaItem>> MediaItemMustExist(GetPosterContents request) =>
(await _mediaItemRepository.Get(request.MediaItemId))
.ToValidation<BaseError>($"MediaItem {request.MediaItemId} does not exist.");
private static Validation<BaseError, MediaItem> PosterPathMustExist(MediaItem mediaItem) =>
Optional(mediaItem.PosterPath)
.Filter(p => !string.IsNullOrWhiteSpace(p) && File.Exists(p))
.Map(_ => mediaItem)
.ToValidation<BaseError>($"MediaItem {mediaItem.Id} does not have a poster");
}
}

2
ErsatzTV.Core/AggregateModels/MediaItemSummary.cs

@ -1,4 +1,4 @@ @@ -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);
}

1
ErsatzTV.Core/Domain/MediaItem.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Domain @@ -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<SimpleMediaCollection> SimpleMediaCollections { get; set; }

10
ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface ILocalPosterProvider
{
Task RefreshPoster(MediaItem mediaItem);
}
}

4
ErsatzTV.Core/Metadata/LocalMediaScanner.cs

@ -16,6 +16,7 @@ namespace ErsatzTV.Core.Metadata @@ -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<LocalMediaScanner> _logger;
private readonly IMediaItemRepository _mediaItemRepository;
@ -28,6 +29,7 @@ namespace ErsatzTV.Core.Metadata @@ -28,6 +29,7 @@ namespace ErsatzTV.Core.Metadata
IPlayoutRepository playoutRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
ILocalPosterProvider localPosterProvider,
ISmartCollectionBuilder smartCollectionBuilder,
IPlayoutBuilder playoutBuilder,
ILogger<LocalMediaScanner> logger)
@ -36,6 +38,7 @@ namespace ErsatzTV.Core.Metadata @@ -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 @@ -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);
}
}

54
ErsatzTV.Core/Metadata/LocalPosterProvider.cs

@ -0,0 +1,54 @@ @@ -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<string> 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<string> RefreshMoviePoster(MediaItem mediaItem)
{
string folder = Path.GetDirectoryName(mediaItem.Path);
if (folder != null)
{
string[] possiblePaths =
{ "poster.jpg", Path.GetFileNameWithoutExtension(mediaItem.Path) + "-poster.jpg" };
Option<string> maybePoster =
possiblePaths.Map(p => Path.Combine(folder, p)).FirstOrDefault(File.Exists);
return maybePoster;
}
return None;
}
private Option<string> RefreshTelevisionPoster(MediaItem mediaItem) => None;
}
}

2
ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs

@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class GenericIntegerIdConfiguration : IEntityTypeConfiguration<GenericIntegerId>
{
public void Configure(EntityTypeBuilder<GenericIntegerId> builder) =>
builder.HasNoKey();
builder.HasNoKey().ToView(null);
}
}

2
ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs

@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class MediaCollectionSummaryConfiguration : IEntityTypeConfiguration<MediaCollectionSummary>
{
public void Configure(EntityTypeBuilder<MediaCollectionSummary> builder) =>
builder.HasNoKey();
builder.HasNoKey().ToView(null);
}
}

2
ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs

@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class MediaItemSummaryConfiguration : IEntityTypeConfiguration<MediaItemSummary>
{
public void Configure(EntityTypeBuilder<MediaItemSummary> builder) =>
builder.HasNoKey();
builder.HasNoKey().ToView(null);
}
}

14
ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs

@ -45,7 +45,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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}", @@ -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

5
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -40,6 +40,11 @@ namespace ErsatzTV.Infrastructure.Data @@ -40,6 +40,11 @@ namespace ErsatzTV.Infrastructure.Data
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Ignore<MediaCollectionSummary>();
builder.Ignore<GenericIntegerId>();
builder.Ignore<MediaItemSummary>();
builder.ApplyConfigurationsFromAssembly(typeof(TvContext).Assembly);
}
}

14
ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs

@ -7,9 +7,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -7,9 +7,9 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
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) @@ -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");
}
}

857
ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.Designer.cs generated

@ -0,0 +1,857 @@ @@ -0,0 +1,857 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("FFmpegProfileId")
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("StreamingMode")
.HasColumnType("INTEGER");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("ConfigElements");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AudioBitrate")
.HasColumnType("INTEGER");
b.Property<int>("AudioBufferSize")
.HasColumnType("INTEGER");
b.Property<int>("AudioChannels")
.HasColumnType("INTEGER");
b.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b.Property<int>("AudioSampleRate")
.HasColumnType("INTEGER");
b.Property<int>("AudioVolume")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("NormalizeAudio")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeAudioCodec")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeResolution")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeVideoCodec")
.HasColumnType("INTEGER");
b.Property<int>("ResolutionId")
.HasColumnType("INTEGER");
b.Property<int>("ThreadCount")
.HasColumnType("INTEGER");
b.Property<bool>("Transcode")
.HasColumnType("INTEGER");
b.Property<int>("VideoBitrate")
.HasColumnType("INTEGER");
b.Property<int>("VideoBufferSize")
.HasColumnType("INTEGER");
b.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ResolutionId");
b.ToTable("FFmpegProfiles");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaCollections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastWriteTime")
.HasColumnType("TEXT");
b.Property<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("PosterPath")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaSourceId");
b.ToTable("MediaItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("SourceType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChannelId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramSchedulePlayoutType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId");
b.HasIndex("ProgramScheduleId");
b.ToTable("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Finish")
.HasColumnType("TEXT");
b.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Start")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaItemId");
b.HasIndex("PlayoutId");
b.ToTable("PlayoutItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
{
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Uri")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceConnections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceLibraries");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionPlaybackOrder")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ProgramSchedules");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<TimeSpan?>("StartTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaCollectionId");
b.HasIndex("ProgramScheduleId");
b.ToTable("ProgramScheduleItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Resolutions");
});
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
{
b.Property<int>("ItemsId")
.HasColumnType("INTEGER");
b.Property<int>("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<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("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<string>("Folder")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.ToTable("LocalMediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("ClientIdentifier")
.HasColumnType("TEXT");
b.Property<string>("ProductVersion")
.HasColumnType("TEXT");
b.ToTable("PlexMediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<bool>("OfflineTail")
.HasColumnType("INTEGER");
b.Property<TimeSpan>("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<int>("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<int>("MediaItemId")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("Aired")
.HasColumnType("TEXT");
b1.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b1.Property<string>("ContentRating")
.HasColumnType("TEXT");
b1.Property<string>("Description")
.HasColumnType("TEXT");
b1.Property<string>("DisplayAspectRatio")
.HasColumnType("TEXT");
b1.Property<TimeSpan>("Duration")
.HasColumnType("TEXT");
b1.Property<int?>("EpisodeNumber")
.HasColumnType("INTEGER");
b1.Property<int>("Height")
.HasColumnType("INTEGER");
b1.Property<int>("MediaType")
.HasColumnType("INTEGER");
b1.Property<string>("SampleAspectRatio")
.HasColumnType("TEXT");
b1.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b1.Property<string>("SortTitle")
.HasColumnType("TEXT");
b1.Property<string>("Subtitle")
.HasColumnType("TEXT");
b1.Property<string>("Title")
.HasColumnType("TEXT");
b1.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b1.Property<int>("VideoScanType")
.HasColumnType("INTEGER");
b1.Property<int>("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<int>("PlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("NextScheduleItemId")
.HasColumnType("INTEGER");
b1.Property<DateTimeOffset>("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<int>("PlayoutProgramScheduleAnchorPlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorProgramScheduleId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorMediaCollectionId")
.HasColumnType("INTEGER");
b1.Property<int>("Index")
.HasColumnType("INTEGER");
b1.Property<int>("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
}
}
}

48
ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.cs

@ -0,0 +1,48 @@ @@ -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<string>(
"PosterPath",
"MediaItems",
"TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
"PosterPath",
"MediaItems");
migrationBuilder.CreateTable(
"GenericIntegerIds",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
},
constraints: table => { });
migrationBuilder.CreateTable(
"MediaCollectionSummaries",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false),
IsSimple = table.Column<bool>("INTEGER", nullable: false),
ItemCount = table.Column<int>("INTEGER", nullable: false),
Name = table.Column<string>("TEXT", nullable: true)
},
constraints: table => { });
}
}
}

32
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -16,35 +16,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -16,35 +16,6 @@ namespace ErsatzTV.Infrastructure.Migrations
modelBuilder
.HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity(
"ErsatzTV.Core.AggregateModels.GenericIntegerId",
b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.ToTable("GenericIntegerIds");
});
modelBuilder.Entity(
"ErsatzTV.Core.AggregateModels.MediaCollectionSummary",
b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<bool>("IsSimple")
.HasColumnType("INTEGER");
b.Property<int>("ItemCount")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.ToTable("MediaCollectionSummaries");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Channel",
b =>
@ -205,6 +176,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -205,6 +176,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("PosterPath")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaSourceId");

28
ErsatzTV/Controllers/PostersController.cs

@ -0,0 +1,28 @@ @@ -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<IActionResult> ForMediaItem(int mediaItemId)
{
Either<BaseError, ImageViewModel> imageContents = await _mediator.Send(new GetPosterContents(mediaItemId));
return imageContents.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new FileContentResult(r.Contents, r.MimeType));
}
}
}

1
ErsatzTV/Services/WorkerService.cs

@ -62,6 +62,7 @@ namespace ErsatzTV.Services @@ -62,6 +62,7 @@ namespace ErsatzTV.Services
RefreshMediaItemMetadata => "metadata",
RefreshMediaItemStatistics => "statistics",
RefreshMediaItemCollections => "collections",
RefreshMediaItemPoster => "poster",
_ => ""
};

18
ErsatzTV/Shared/MediaCard.razor

@ -1,8 +1,16 @@ @@ -1,8 +1,16 @@
<div class="media-card-container mx-3 pb-3">
@using ErsatzTV.Application.MediaItems
<div class="media-card-container mx-3 pb-3">
<MudPaper Class="media-card">
<MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-disabled">
@Placeholder(Data.SortTitle)
</MudText>
@if (Data.HasPoster)
{
<img src="@($"/posters/{Data.MediaItemId}")" style="max-height: 220px"/>
}
else
{
<MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-disabled">
@Placeholder(Data.SortTitle)
</MudText>
}
</MudPaper>
<MudText Align="Align.Center" Class="media-card-title" UserAttributes="@(new Dictionary<string, object> { { "title", Data.Title } })">
@Data.Title
@ -15,7 +23,7 @@ @@ -15,7 +23,7 @@
@code {
[Parameter]
public IMediaCard Data { get; set; }
public AggregateMediaItemViewModel Data { get; set; }
private string Placeholder(string sortTitle)
{

4
ErsatzTV/Shared/MediaItemsGrid.razor

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px">
<MudPaper Style="display: flex; justify-content: center; align-items: center">
<MudPaper Style="align-items: center; display: flex; justify-content: center;">
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
OnClick="@(() => PrevPage())"
Disabled="@(_pageNumber <= 1)">
@ -45,7 +45,7 @@ @@ -45,7 +45,7 @@
_pageNumber -= 1;
await RefreshData();
}
private async Task NextPage()
{
_pageNumber += 1;

1
ErsatzTV/Startup.cs

@ -164,6 +164,7 @@ namespace ErsatzTV @@ -164,6 +164,7 @@ namespace ErsatzTV
services.AddScoped<ISmartCollectionBuilder, SmartCollectionBuilder>();
services.AddScoped<ILocalMetadataProvider, LocalMetadataProvider>();
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();
services.AddScoped<ILocalPosterProvider, LocalPosterProvider>();
services.AddScoped<ILocalMediaScanner, LocalMediaScanner>();
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();

8
ErsatzTV/wwwroot/css/site.css

@ -4,9 +4,7 @@ @@ -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 @@ @@ -23,6 +21,4 @@
width: 100%;
}
.media-card-poster-placeholder {
font-weight: bold;
}
.media-card-poster-placeholder { font-weight: bold; }
Loading…
Cancel
Save