147 lines
5.7 KiB
C#
147 lines
5.7 KiB
C#
using Data.Data.Repositories;
|
|
using Data.Errors;
|
|
using DeepDrftData.Data;
|
|
using DeepDrftModels.DTOs;
|
|
using DeepDrftModels.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Models.Common;
|
|
|
|
namespace DeepDrftData.Repositories;
|
|
|
|
public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
|
|
{
|
|
public TrackRepository(
|
|
DeepDrftContext context,
|
|
ILogger<Repository<DeepDrftContext, TrackEntity>> logger,
|
|
IDbExceptionClassifier? classifier = null)
|
|
: base(context, logger, classifier: classifier)
|
|
{
|
|
}
|
|
|
|
// Lookup by vault entry key. The base Repository<> only exposes id-based queries, so this
|
|
// uses Query (soft-delete filtered) rather than the raw DbSet.
|
|
public async Task<TrackEntity?> GetByEntryKeyAsync(string entryKey)
|
|
=> await Query.FirstOrDefaultAsync(t => t.EntryKey == entryKey);
|
|
|
|
// Picks one track uniformly at random. Two round-trips (count, then a single offset row)
|
|
// rather than ORDER BY random() so the database never sorts the whole table — the catalogue
|
|
// is small today but this keeps the cost flat as it grows. Returns null when empty so the
|
|
// service surfaces a valid empty-library state, not an error. Uses Query (soft-delete
|
|
// filtered) so deleted tracks are never candidates.
|
|
public async Task<TrackEntity?> GetRandomAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var count = await Query.CountAsync(cancellationToken);
|
|
if (count == 0)
|
|
return null;
|
|
|
|
var index = Random.Shared.Next(count);
|
|
return await Query
|
|
.OrderBy(t => t.Id)
|
|
.Skip(index)
|
|
.Take(1)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
}
|
|
|
|
// Paged query with optional filter predicates. Built off Query (soft-delete filtered) rather than the
|
|
// base GetPagedAsync(paging) overload, which takes no where-clause. The OrderBy expression and
|
|
// direction ride in on the PagingParameters the manager already built, so sort + filter +
|
|
// pagination compose. Filter predicates apply before sort and Skip/Take so TotalCount reflects
|
|
// the filtered set.
|
|
public async Task<PagedResult<TrackEntity>> GetPagedFilteredAsync(
|
|
PagingParameters<TrackEntity> paging,
|
|
TrackFilter? filter,
|
|
CancellationToken ct = default)
|
|
{
|
|
IQueryable<TrackEntity> query = Query;
|
|
|
|
if (filter is not null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(filter.SearchText))
|
|
{
|
|
// Postgres case-insensitive LIKE. The '%' wraps make it a contains-match; ILike is
|
|
// EF-translatable where ToLower().Contains() is not. Album is nullable — ILike on a
|
|
// null column yields false, which is the desired "no match" behaviour.
|
|
var pattern = $"%{filter.SearchText}%";
|
|
query = query.Where(t =>
|
|
EF.Functions.ILike(t.TrackName, pattern)
|
|
|| EF.Functions.ILike(t.Artist, pattern)
|
|
|| (t.Album != null && EF.Functions.ILike(t.Album, pattern)));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.Album))
|
|
query = query.Where(t => t.Album == filter.Album);
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.Genre))
|
|
query = query.Where(t => t.Genre == filter.Genre);
|
|
}
|
|
|
|
var totalCount = await query.CountAsync(ct);
|
|
|
|
if (paging.OrderBy is not null)
|
|
{
|
|
query = paging.IsDescending
|
|
? query.OrderByDescending(paging.OrderBy)
|
|
: query.OrderBy(paging.OrderBy);
|
|
}
|
|
|
|
var items = await query
|
|
.Skip(paging.Skip)
|
|
.Take(paging.PageSize)
|
|
.ToListAsync(ct);
|
|
|
|
return new PagedResult<TrackEntity>
|
|
{
|
|
Items = items,
|
|
TotalCount = totalCount,
|
|
Page = paging.Page,
|
|
PageSize = paging.PageSize,
|
|
};
|
|
}
|
|
|
|
// Distinct albums (non-null) with track counts and a representative cover key. The cover is the
|
|
// first non-null ImagePath in the group; GroupBy + projection keeps it a single round-trip.
|
|
public async Task<List<AlbumSummaryDto>> GetDistinctAlbumsAsync(CancellationToken ct = default)
|
|
=> await Query
|
|
.Where(t => t.Album != null)
|
|
.GroupBy(t => t.Album!)
|
|
.Select(g => new AlbumSummaryDto
|
|
{
|
|
Album = g.Key,
|
|
TrackCount = g.Count(),
|
|
CoverImageKey = g
|
|
.Where(t => t.ImagePath != null)
|
|
.OrderBy(t => t.Id)
|
|
.Select(t => t.ImagePath)
|
|
.FirstOrDefault(),
|
|
})
|
|
.OrderBy(a => a.Album)
|
|
.ToListAsync(ct);
|
|
|
|
// Distinct genres (non-null) with track counts.
|
|
public async Task<List<GenreSummaryDto>> GetDistinctGenresAsync(CancellationToken ct = default)
|
|
=> await Query
|
|
.Where(t => t.Genre != null)
|
|
.GroupBy(t => t.Genre!)
|
|
.Select(g => new GenreSummaryDto
|
|
{
|
|
Genre = g.Key,
|
|
TrackCount = g.Count(),
|
|
})
|
|
.OrderBy(g => g.Genre)
|
|
.ToListAsync(ct);
|
|
|
|
protected override void UpdateEntity(TrackEntity target, TrackEntity source)
|
|
{
|
|
base.UpdateEntity(target, source); // copies CreatedAt, UpdatedAt, IsDeleted
|
|
target.EntryKey = source.EntryKey;
|
|
target.TrackName = source.TrackName;
|
|
target.Artist = source.Artist;
|
|
target.Album = source.Album;
|
|
target.Genre = source.Genre;
|
|
target.ReleaseDate = source.ReleaseDate;
|
|
target.ImagePath = source.ImagePath;
|
|
target.CreatedByUserId = source.CreatedByUserId;
|
|
}
|
|
}
|