Browse Source

sort and page in the db

pull/22/head
Jason Dove 6 years ago
parent
commit
1d206289d7
  1. 6
      ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs
  2. 8
      ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs
  3. 5
      ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs
  4. 38
      ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs
  5. 4
      ErsatzTV.Core/AggregateModels/MediaItemSummary.cs
  6. 1
      ErsatzTV.Core/Domain/MediaMetadata.cs
  7. 4
      ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs
  8. 4
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  9. 12
      ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs
  10. 12
      ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs
  11. 2
      ErsatzTV.Infrastructure/Data/LogContext.cs
  12. 48
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  13. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  14. 32
      ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs
  15. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  16. 8
      ErsatzTV/Shared/MediaCard.razor
  17. 29
      ErsatzTV/Shared/MediaItemsGrid.razor

6
ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaItems
{
public record AggregateMediaItemResults(int Count, List<AggregateMediaItemViewModel> DataPage);
}

8
ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs

@ -1,13 +1,7 @@ @@ -1,13 +1,7 @@
namespace ErsatzTV.Application.MediaItems
{
public record AggregateMediaItemViewModel(
string Source,
string Title,
string Subtitle,
int Count,
string Duration) : IMediaCard
{
public string SortTitle =>
Title.ToLowerInvariant().StartsWith("the ") ? Title.Substring(4) : Title;
}
string SortTitle) : IMediaCard;
}

5
ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs

@ -1,9 +1,8 @@ @@ -1,9 +1,8 @@
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Queries
{
public record GetAggregateMediaItems
(MediaType MediaType, string SearchString) : IRequest<List<AggregateMediaItemViewModel>>;
(MediaType MediaType, int PageNumber, int PageSize) : IRequest<AggregateMediaItemResults>;
}

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

@ -1,49 +1,37 @@ @@ -1,49 +1,37 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Queries
{
public class
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, List<AggregateMediaItemViewModel>>
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, AggregateMediaItemResults>
{
private readonly IMediaItemRepository _mediaItemRepository;
public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
public async Task<List<AggregateMediaItemViewModel>> Handle(
public async Task<AggregateMediaItemResults> Handle(
GetAggregateMediaItems request,
CancellationToken cancellationToken)
{
IEnumerable<MediaItem> allItems = await _mediaItemRepository.GetAll(request.MediaType);
int count = await _mediaItemRepository.GetCountByType(request.MediaType);
if (!string.IsNullOrEmpty(request.SearchString))
{
allItems = allItems.Filter(
i => i.Metadata?.Title.Contains(request.SearchString, StringComparison.OrdinalIgnoreCase) ==
true);
}
IEnumerable<MediaItemSummary> allItems = await _mediaItemRepository.GetPageByType(
request.MediaType,
request.PageNumber,
request.PageSize);
return allItems.GroupBy(c => new { c.Source.Name, c.Metadata.Title }).Map(
group => new AggregateMediaItemViewModel(
group.Key.Name,
group.Key.Title,
request.MediaType == MediaType.TvShow
? $"{group.Count()} Episodes"
: group.Min(i => i.Metadata?.Aired?.Year).ToString(),
group.Count(),
group.Count() == 1 ? DisplayDuration(group.Head()) : string.Empty))
var results = allItems
.Map(s => new AggregateMediaItemViewModel(s.Title, s.Subtitle, s.SortTitle))
.ToList();
}
private static string DisplayDuration(MediaItem mediaItem) => string.Format(
mediaItem.Metadata?.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata?.Duration);
return new AggregateMediaItemResults(count, results);
}
}
}

4
ErsatzTV.Core/AggregateModels/MediaItemSummary.cs

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

1
ErsatzTV.Core/Domain/MediaMetadata.cs

@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Domain @@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Domain
public string AudioCodec { get; set; }
public MediaType MediaType { get; set; }
public string Title { get; set; }
public string SortTitle { get; set; }
public string Subtitle { get; set; }
public string Description { get; set; }
public int? SeasonNumber { get; set; }

4
ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using LanguageExt;
@ -11,7 +12,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -11,7 +12,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public Task<Option<MediaItem>> Get(int id);
public Task<List<MediaItem>> GetAll();
public Task<List<MediaItem>> Search(string searchString);
public Task<List<MediaItem>> GetAll(MediaType mediaType);
public Task<List<MediaItemSummary>> GetPageByType(MediaType mediaType, int pageNumber, int pageSize);
public Task<int> GetCountByType(MediaType mediaType);
public Task<List<MediaItem>> GetAllByMediaSourceId(int mediaSourceId);
public Task Update(MediaItem mediaItem);
public Task Delete(int mediaItemId);

4
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -43,6 +43,10 @@ namespace ErsatzTV.Core.Metadata @@ -43,6 +43,10 @@ namespace ErsatzTV.Core.Metadata
mediaItem.Metadata.MediaType = metadata.MediaType;
mediaItem.Metadata.Title = metadata.Title;
mediaItem.Metadata.Subtitle = metadata.Subtitle;
mediaItem.Metadata.SortTitle =
(metadata.Title ?? string.Empty).ToLowerInvariant().StartsWith("the ")
? metadata.Title?.Substring(4)
: metadata.Title;
mediaItem.Metadata.Description = metadata.Description;
mediaItem.Metadata.EpisodeNumber = metadata.EpisodeNumber;
mediaItem.Metadata.SeasonNumber = metadata.SeasonNumber;

12
ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs

@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class LogEntryConfiguration : IEntityTypeConfiguration<LogEntry>
{
public void Configure(EntityTypeBuilder<LogEntry> builder) =>
builder.ToTable("Logs");
}
}

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

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using ErsatzTV.Core.AggregateModels;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaItemSummaryConfiguration : IEntityTypeConfiguration<MediaItemSummary>
{
public void Configure(EntityTypeBuilder<MediaItemSummary> builder) =>
builder.HasNoKey();
}
}

2
ErsatzTV.Infrastructure/Data/LogContext.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Data @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Data
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurationsFromAssembly(typeof(LogContext).Assembly);
builder.Entity<LogEntry>().ToTable("Logs");
}
}
}

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@ -40,11 +41,48 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -40,11 +41,48 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
public Task<List<MediaItem>> GetAll(MediaType mediaType) =>
_dbContext.MediaItems
.Include(i => i.Source)
.Filter(i => i.Metadata.MediaType == mediaType)
.ToListAsync();
public Task<List<MediaItemSummary>> GetPageByType(MediaType mediaType, int pageNumber, int pageSize) =>
mediaType switch
{
MediaType.Movie => _dbContext.MediaItems
.AsNoTracking()
.Filter(i => i.Metadata.MediaType == mediaType)
.OrderBy(i => i.Metadata.SortTitle)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync()
.Map(
list => list.Map(
i => new MediaItemSummary(
i.Metadata.Title,
i.Metadata.SortTitle,
i.Metadata.Aired?.Year.ToString()))
.ToList()),
MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw(
@"SELECT Metadata_Title AS Title, Metadata_SortTitle AS SortTitle, count(*) || ' Episodes' AS Subtitle
FROM MediaItems WHERE Metadata_MediaType=1
GROUP BY Metadata_Title, Metadata_SortTitle
ORDER BY Metadata_SortTitle
LIMIT {0} OFFSET {1}",
pageSize,
(pageNumber - 1) * pageSize)
.AsNoTracking()
.ToListAsync(),
_ => Task.FromResult(new List<MediaItemSummary>())
};
public Task<int> GetCountByType(MediaType mediaType) =>
mediaType switch
{
MediaType.Movie => _dbContext.MediaItems
.Filter(i => i.Metadata.MediaType == mediaType)
.CountAsync(),
MediaType.TvShow => _dbContext.MediaItems
.Filter(i => i.Metadata.MediaType == mediaType)
.GroupBy(i => new { i.Metadata.Title, i.Metadata.SortTitle })
.CountAsync(),
_ => Task.FromResult(0)
};
public Task<List<MediaItem>> GetAllByMediaSourceId(int mediaSourceId) =>
_dbContext.MediaItems

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -32,6 +32,7 @@ namespace ErsatzTV.Infrastructure.Data @@ -32,6 +32,7 @@ namespace ErsatzTV.Infrastructure.Data
// support raw sql queries
public DbSet<MediaCollectionSummary> MediaCollectionSummaries { get; set; }
public DbSet<GenericIntegerId> GenericIntegerIds { get; set; }
public DbSet<MediaItemSummary> MediaItemSummaries { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder.UseLoggerFactory(_loggerFactory);

32
ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class MetadataSortTitle : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Metadata_SortTitle",
table: "MediaItems",
type: "TEXT",
nullable: true);
migrationBuilder.Sql(
@"UPDATE MediaItems
SET Metadata_SortTitle = Metadata_Title");
migrationBuilder.Sql(
@"UPDATE MediaItems
SET Metadata_SortTitle = substr(Metadata_Title, 5)
WHERE Metadata_Title LIKE 'the %'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Metadata_SortTitle",
table: "MediaItems");
}
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -631,6 +631,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -631,6 +631,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b1.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b1.Property<string>("SortTitle")
.HasColumnType("TEXT");
b1.Property<string>("Subtitle")
.HasColumnType("TEXT");

8
ErsatzTV/Shared/MediaCard.razor

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
<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">
@Data.SortTitle.Substring(0, 1)
@Placeholder(Data.SortTitle)
</MudText>
</MudPaper>
<MudText Align="Align.Center" Class="media-card-title" UserAttributes="@(new Dictionary<string, object> { { "title", Data.Title } })">
@ -17,4 +17,10 @@ @@ -17,4 +17,10 @@
[Parameter]
public IMediaCard Data { get; set; }
private string Placeholder(string sortTitle)
{
string first = sortTitle.Substring(0, 1).ToUpperInvariant();
return int.TryParse(first, out _) ? "#" : first;
}
}

29
ErsatzTV/Shared/MediaItemsGrid.razor

@ -5,36 +5,51 @@ @@ -5,36 +5,51 @@
<MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px">
<MudPaper Style="display: flex; justify-content: center; align-items: center">
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
OnClick="@(() => _pageNumber -= 1)"
OnClick="@(() => PrevPage())"
Disabled="@(_pageNumber <= 1)">
</MudIconButton>
<MudText Style="flex-grow: 1"
Align="Align.Center">
@((_pageNumber - 1) * PageSize + 1)-@Math.Min(_data.Count, _pageNumber * PageSize) of @_data.Count
@Math.Min((_pageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * PageSize) of @_data.Count
</MudText>
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight"
OnClick="@(() => _pageNumber += 1)" Disabled="@(_pageNumber * PageSize > _data.Count)">
OnClick="@(() => NextPage())" Disabled="@(_pageNumber * PageSize >= _data.Count)">
</MudIconButton>
</MudPaper>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (AggregateMediaItemViewModel item in _data.OrderBy(i => i.SortTitle).Skip(PageSize * (_pageNumber - 1)).Take(PageSize))
@foreach (AggregateMediaItemViewModel item in _data.DataPage)
{
<MediaCard Data="@item"/>
}
</MudContainer>
@code {
[Parameter]
public MediaType MediaType { get; set; }
private int PageSize => 100;
private int _pageNumber = 1;
private List<AggregateMediaItemViewModel> _data;
private AggregateMediaItemResults _data;
protected override Task OnParametersSetAsync() => RefreshData();
protected override async Task OnInitializedAsync() =>
_data = await Mediator.Send(new GetAggregateMediaItems(MediaType, string.Empty));
private async Task RefreshData() =>
_data = await Mediator.Send(new GetAggregateMediaItems(MediaType, _pageNumber, PageSize));
private async Task PrevPage()
{
_pageNumber -= 1;
await RefreshData();
}
private async Task NextPage()
{
_pageNumber += 1;
await RefreshData();
}
}
Loading…
Cancel
Save