Browse Source

add metadata/poster refresh button to media cards

pull/23/head
Jason Dove 6 years ago
parent
commit
134e8d08a8
  1. 1
      ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs
  2. 45
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs
  3. 1
      ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs
  4. 2
      ErsatzTV.Core/AggregateModels/MediaItemSummary.cs
  5. 3
      ErsatzTV.Core/Metadata/LocalMediaScanner.cs
  6. 9
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  7. 2
      ErsatzTV/ErsatzTV.csproj
  8. 29
      ErsatzTV/Shared/MediaCard.razor
  9. 2
      ErsatzTV/Shared/MediaItemsGrid.razor
  10. 15
      ErsatzTV/wwwroot/css/site.css

1
ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs

@ -1,6 +1,7 @@
namespace ErsatzTV.Application.MediaItems namespace ErsatzTV.Application.MediaItems
{ {
public record AggregateMediaItemViewModel( public record AggregateMediaItemViewModel(
int MediaItemId,
string Title, string Title,
string Subtitle, string Subtitle,
string SortTitle, string SortTitle,

45
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs

@ -0,0 +1,45 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaItems.Commands
{
public class
RefreshMediaItemPosterHandler : MediatR.IRequestHandler<RefreshMediaItemPoster,
Either<BaseError, Unit>>
{
private readonly ILocalPosterProvider _localPosterProvider;
private readonly IMediaItemRepository _mediaItemRepository;
public RefreshMediaItemPosterHandler(
IMediaItemRepository mediaItemRepository,
ILocalPosterProvider localPosterProvider)
{
_mediaItemRepository = mediaItemRepository;
_localPosterProvider = localPosterProvider;
}
public Task<Either<BaseError, Unit>> Handle(
RefreshMediaItemPoster request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(RefreshPoster)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemPoster request) =>
MediaItemMustExist(request);
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(RefreshMediaItemPoster request) =>
_mediaItemRepository.Get(request.MediaItemId)
.Map(
maybeItem => maybeItem.ToValidation<BaseError>(
$"[MediaItem] {request.MediaItemId} does not exist."));
private Task<Unit> RefreshPoster(MediaItem mediaItem) =>
_localPosterProvider.RefreshPoster(mediaItem).ToUnit();
}
}

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

@ -30,6 +30,7 @@ namespace ErsatzTV.Application.MediaItems.Queries
var results = allItems var results = allItems
.Map( .Map(
s => new AggregateMediaItemViewModel( s => new AggregateMediaItemViewModel(
s.MediaItemId,
s.Title, s.Title,
s.Subtitle, s.Subtitle,
s.SortTitle, s.SortTitle,

2
ErsatzTV.Core/AggregateModels/MediaItemSummary.cs

@ -1,4 +1,4 @@
namespace ErsatzTV.Core.AggregateModels namespace ErsatzTV.Core.AggregateModels
{ {
public record MediaItemSummary(string Title, string SortTitle, string Subtitle, string Poster); public record MediaItemSummary(int MediaItemId, string Title, string SortTitle, string Subtitle, string Poster);
} }

3
ErsatzTV.Core/Metadata/LocalMediaScanner.cs

@ -102,7 +102,8 @@ namespace ErsatzTV.Core.Metadata
{ {
DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path); DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path);
bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue); bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue);
return modified || mediaItem.Metadata == null || mediaItem.Metadata.MediaType != localMediaSource.MediaType; return modified || mediaItem.Metadata == null ||
mediaItem.Metadata.MediaType != localMediaSource.MediaType;
}); });
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems)); modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
foreach (MediaItem mediaItem in modifiedMediaItems) foreach (MediaItem mediaItem in modifiedMediaItems)

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

@ -24,7 +24,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
public Task<Option<MediaItem>> Get(int id) => public Task<Option<MediaItem>> Get(int id) =>
_dbContext.MediaItems.SingleOrDefaultAsync(i => i.Id == id).Map(Optional); _dbContext.MediaItems
.Include(i => i.Source)
.SingleOrDefaultAsync(i => i.Id == id)
.Map(Optional);
public Task<List<MediaItem>> GetAll() => _dbContext.MediaItems.ToListAsync(); public Task<List<MediaItem>> GetAll() => _dbContext.MediaItems.ToListAsync();
@ -46,6 +49,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
MediaType.Movie => _dbContext.MediaItemSummaries.FromSqlRaw( MediaType.Movie => _dbContext.MediaItemSummaries.FromSqlRaw(
@"SELECT @"SELECT
Id AS MediaItemId,
Metadata_Title AS Title, Metadata_Title AS Title,
Metadata_SortTitle AS SortTitle, Metadata_SortTitle AS SortTitle,
substr(Metadata_Aired, 1, 4) AS Subtitle, substr(Metadata_Aired, 1, 4) AS Subtitle,
@ -59,10 +63,11 @@ LIMIT {0} OFFSET {1}",
.ToListAsync(), .ToListAsync(),
MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw( MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw(
@"SELECT @"SELECT
min(Id) AS MediaItemId,
Metadata_Title AS Title, Metadata_Title AS Title,
Metadata_SortTitle AS SortTitle, Metadata_SortTitle AS SortTitle,
count(*) || ' Episodes' AS Subtitle, count(*) || ' Episodes' AS Subtitle,
min(Poster) AS Poster max(Poster) AS Poster
FROM MediaItems WHERE Metadata_MediaType=1 FROM MediaItems WHERE Metadata_MediaType=1
GROUP BY Metadata_Title, Metadata_SortTitle GROUP BY Metadata_Title, Metadata_SortTitle
ORDER BY Metadata_SortTitle ORDER BY Metadata_SortTitle

2
ErsatzTV/ErsatzTV.csproj

@ -20,7 +20,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MudBlazor" Version="5.0.0" /> <PackageReference Include="MudBlazor" Version="5.0.1" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.1" /> <PackageReference Include="Refit.HttpClientFactory" Version="6.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

29
ErsatzTV/Shared/MediaCard.razor

@ -1,16 +1,17 @@
@using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.MediaItems.Commands
@using Unit = LanguageExt.Unit
@inject IMediator Mediator
<div class="media-card-container mx-3 pb-3"> <div class="media-card-container mx-3 pb-3">
<MudPaper Class="media-card"> <MudPaper Class="media-card" Style="@PosterForItem()">
@if (!string.IsNullOrWhiteSpace(Data.Poster)) @if (string.IsNullOrWhiteSpace(Data.Poster))
{
<img src="@($"/posters/{Data.Poster}")" style="max-height: 220px"/>
}
else
{ {
<MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-disabled"> <MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-disabled">
@Placeholder(Data.SortTitle) @Placeholder(Data.SortTitle)
</MudText> </MudText>
} }
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Color="Color.Primary" OnClick="@(() => RefreshMetadata())" Class="media-card-menu"></MudIconButton>
</MudPaper> </MudPaper>
<MudText Align="Align.Center" Class="media-card-title" UserAttributes="@(new Dictionary<string, object> { { "title", Data.Title } })"> <MudText Align="Align.Center" Class="media-card-title" UserAttributes="@(new Dictionary<string, object> { { "title", Data.Title } })">
@Data.Title @Data.Title
@ -25,10 +26,26 @@
[Parameter] [Parameter]
public AggregateMediaItemViewModel Data { get; set; } public AggregateMediaItemViewModel Data { get; set; }
[Parameter]
public EventCallback<Unit> DataRefreshed { get; set; }
private string Placeholder(string sortTitle) private string Placeholder(string sortTitle)
{ {
string first = sortTitle.Substring(0, 1).ToUpperInvariant(); string first = sortTitle.Substring(0, 1).ToUpperInvariant();
return int.TryParse(first, out _) ? "#" : first; return int.TryParse(first, out _) ? "#" : first;
} }
private string PosterForItem() => string.IsNullOrWhiteSpace(Data.Poster)
? "position: relative"
: $"position: relative; background-image: url(/posters/{Data.Poster}); background-size: cover";
private async Task RefreshMetadata()
{
// TODO: how should we refresh an entire television show?
await Mediator.Send(new RefreshMediaItemMetadata(Data.MediaItemId));
await Mediator.Send(new RefreshMediaItemCollections(Data.MediaItemId));
await Mediator.Send(new RefreshMediaItemPoster(Data.MediaItemId));
await DataRefreshed.InvokeAsync();
}
} }

2
ErsatzTV/Shared/MediaItemsGrid.razor

@ -21,7 +21,7 @@
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (AggregateMediaItemViewModel item in _data.DataPage) @foreach (AggregateMediaItemViewModel item in _data.DataPage)
{ {
<MediaCard Data="@item"/> <MediaCard Data="@item" DataRefreshed="@(() => RefreshData())"/>
} }
</MudContainer> </MudContainer>

15
ErsatzTV/wwwroot/css/site.css

@ -8,12 +8,16 @@
.media-card { .media-card {
display: flex; display: flex;
filter: brightness(100%);
flex-direction: column; flex-direction: column;
height: 220px; height: 220px;
justify-content: center; justify-content: center;
transition: all 0.2s ease;
width: 152px; width: 152px;
} }
.media-card:hover { filter: brightness(80%); }
.media-card-title { .media-card-title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -21,4 +25,13 @@
width: 100%; width: 100%;
} }
.media-card-poster-placeholder { font-weight: bold; } .media-card-poster-placeholder { font-weight: bold; }
.media-card-menu {
bottom: 0;
display: none;
position: absolute;
right: 0;
}
.media-card:hover .media-card-menu { display: block; }
Loading…
Cancel
Save