From 1d206289d7edf5a6898a5eb0e7363f5f668e0e5d Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sat, 13 Feb 2021 11:08:07 -0600 Subject: [PATCH] sort and page in the db --- .../MediaItems/AggregateMediaItemResults.cs | 6 +++ .../MediaItems/AggregateMediaItemViewModel.cs | 8 +--- .../Queries/GetAggregateMediaItems.cs | 5 +- .../Queries/GetAggregateMediaItemsHandler.cs | 38 +++++---------- .../AggregateModels/MediaItemSummary.cs | 4 ++ ErsatzTV.Core/Domain/MediaMetadata.cs | 1 + .../Repositories/IMediaItemRepository.cs | 4 +- .../Metadata/LocalMetadataProvider.cs | 4 ++ .../Configurations/LogEntryConfiguration.cs | 12 ----- .../MediaItemSummaryConfiguration.cs | 12 +++++ ErsatzTV.Infrastructure/Data/LogContext.cs | 2 +- .../Data/Repositories/MediaItemRepository.cs | 48 +++++++++++++++++-- ErsatzTV.Infrastructure/Data/TvContext.cs | 1 + .../20210213155419_MetadataSortTitle.cs | 32 +++++++++++++ .../Migrations/TvContextModelSnapshot.cs | 3 ++ ErsatzTV/Shared/MediaCard.razor | 8 +++- ErsatzTV/Shared/MediaItemsGrid.razor | 29 ++++++++--- 17 files changed, 155 insertions(+), 62 deletions(-) create mode 100644 ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs create mode 100644 ErsatzTV.Core/AggregateModels/MediaItemSummary.cs delete mode 100644 ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs diff --git a/ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs b/ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs new file mode 100644 index 000000000..9c38e3ea7 --- /dev/null +++ b/ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaItems +{ + public record AggregateMediaItemResults(int Count, List DataPage); +} diff --git a/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs b/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs index a768b3fcf..c884fa37a 100644 --- a/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs +++ b/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs @@ -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; } diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs index cfd1f2dc8..c552bf0cd 100644 --- a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs +++ b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs @@ -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>; + (MediaType MediaType, int PageNumber, int PageSize) : IRequest; } diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs index 643f7ca9f..b9bc8aa8d 100644 --- a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs +++ b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs @@ -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> + GetAggregateMediaItemsHandler : IRequestHandler { private readonly IMediaItemRepository _mediaItemRepository; public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) => _mediaItemRepository = mediaItemRepository; - public async Task> Handle( + public async Task Handle( GetAggregateMediaItems request, CancellationToken cancellationToken) { - IEnumerable 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 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); + } } } diff --git a/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs b/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs new file mode 100644 index 000000000..7d8b9174f --- /dev/null +++ b/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Core.AggregateModels +{ + public record MediaItemSummary(string Title, string SortTitle, string Subtitle); +} diff --git a/ErsatzTV.Core/Domain/MediaMetadata.cs b/ErsatzTV.Core/Domain/MediaMetadata.cs index 30d93bf1a..efe2774bd 100644 --- a/ErsatzTV.Core/Domain/MediaMetadata.cs +++ b/ErsatzTV.Core/Domain/MediaMetadata.cs @@ -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; } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs index 9c6961c4c..1f09bb9a7 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs @@ -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 public Task> Get(int id); public Task> GetAll(); public Task> Search(string searchString); - public Task> GetAll(MediaType mediaType); + public Task> GetPageByType(MediaType mediaType, int pageNumber, int pageSize); + public Task GetCountByType(MediaType mediaType); public Task> GetAllByMediaSourceId(int mediaSourceId); public Task Update(MediaItem mediaItem); public Task Delete(int mediaItemId); diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index cf364add7..9846c891f 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -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; diff --git a/ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs deleted file mode 100644 index e79223e0d..000000000 --- a/ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs +++ /dev/null @@ -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 - { - public void Configure(EntityTypeBuilder builder) => - builder.ToTable("Logs"); - } -} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs new file mode 100644 index 000000000..cd7c9ef09 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs @@ -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 + { + public void Configure(EntityTypeBuilder builder) => + builder.HasNoKey(); + } +} diff --git a/ErsatzTV.Infrastructure/Data/LogContext.cs b/ErsatzTV.Infrastructure/Data/LogContext.cs index 875bf84bd..67680cd93 100644 --- a/ErsatzTV.Infrastructure/Data/LogContext.cs +++ b/ErsatzTV.Infrastructure/Data/LogContext.cs @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Data protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); - builder.ApplyConfigurationsFromAssembly(typeof(LogContext).Assembly); + builder.Entity().ToTable("Logs"); } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs index 670b67d81..7fd50e500 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs @@ -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 } - public Task> GetAll(MediaType mediaType) => - _dbContext.MediaItems - .Include(i => i.Source) - .Filter(i => i.Metadata.MediaType == mediaType) - .ToListAsync(); + public Task> 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()) + }; + + public Task 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> GetAllByMediaSourceId(int mediaSourceId) => _dbContext.MediaItems diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index a8866e1f5..e3471a62b 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -32,6 +32,7 @@ namespace ErsatzTV.Infrastructure.Data // support raw sql queries public DbSet MediaCollectionSummaries { get; set; } public DbSet GenericIntegerIds { get; set; } + public DbSet MediaItemSummaries { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseLoggerFactory(_loggerFactory); diff --git a/ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs b/ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs new file mode 100644 index 000000000..0eab76ea6 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.cs @@ -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( + 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"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 252da1d5b..7de29978c 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -631,6 +631,9 @@ namespace ErsatzTV.Infrastructure.Migrations b1.Property("SeasonNumber") .HasColumnType("INTEGER"); + b1.Property("SortTitle") + .HasColumnType("TEXT"); + b1.Property("Subtitle") .HasColumnType("TEXT"); diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 6871b4e18..224f74d97 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -1,7 +1,7 @@ 
- @Data.SortTitle.Substring(0, 1) + @Placeholder(Data.SortTitle) @@ -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; + } + } \ No newline at end of file diff --git a/ErsatzTV/Shared/MediaItemsGrid.razor b/ErsatzTV/Shared/MediaItemsGrid.razor index 34ed01717..09e355bbe 100644 --- a/ErsatzTV/Shared/MediaItemsGrid.razor +++ b/ErsatzTV/Shared/MediaItemsGrid.razor @@ -5,36 +5,51 @@ - @((_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 + OnClick="@(() => NextPage())" Disabled="@(_pageNumber * PageSize >= _data.Count)"> - @foreach (AggregateMediaItemViewModel item in _data.OrderBy(i => i.SortTitle).Skip(PageSize * (_pageNumber - 1)).Take(PageSize)) + @foreach (AggregateMediaItemViewModel item in _data.DataPage) { } @code { + [Parameter] public MediaType MediaType { get; set; } private int PageSize => 100; private int _pageNumber = 1; - private List _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(); + } } \ No newline at end of file