Files
jsmr/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs
Brian Bicknell 2418bd0a8f
All checks were successful
ci / build-test (push) Successful in 2m17s
ci / publish-image (push) Has been skipped
Updated search logic. More UI updates.
2025-11-17 21:05:55 -05:00

487 lines
22 KiB
C#

using JSMR.Application.Common.Search;
using JSMR.Application.VoiceWorks.Queries.Search;
using JSMR.Domain.Entities;
using JSMR.Domain.Enums;
using JSMR.Infrastructure.Common.Queries;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
using CircleStatus = JSMR.Application.VoiceWorks.Queries.Search.CircleStatus;
namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks;
public class VoiceWorkQuery
{
public required VoiceWork VoiceWork { get; init; }
public EnglishVoiceWork? EnglishVoiceWork { get; init; }
public required Circle Circle { get; init; }
//public VoiceWorkLocalization? VoiceWorkLocalization { get; init; }
//public VoiceWorkSearch? VoiceWorkSearch { get; init; }
}
public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSearch fullTextSearch) : SearchProvider<VoiceWorkSearchResult, VoiceWorkSearchCriteria, VoiceWorkSortField, VoiceWorkQuery>, IVoiceWorkSearchProvider
{
protected override bool UseSelectIdQuery => true;
protected override IQueryable<VoiceWorkQuery> GetBaseQuery()
{
return
from voiceWork in context.VoiceWorks.AsNoTracking()
join englishVoiceWork in context.EnglishVoiceWorks.AsNoTracking() on voiceWork.VoiceWorkId equals englishVoiceWork.VoiceWorkId into ps
from englishVoiceWork in ps.DefaultIfEmpty()
join circle in context.Circles.AsNoTracking() on voiceWork.CircleId equals circle.CircleId into cs
from circle in cs.DefaultIfEmpty()
//join voiceWorkLocalization in context.VoiceWorkLocalizations on voiceWork.VoiceWorkId equals voiceWorkLocalization.VoiceWorkId into vwl
//from voiceWorkLocalization in vwl.DefaultIfEmpty()
//join voiceWorkSearch in context.VoiceWorkSearches on voiceWork.VoiceWorkId equals voiceWorkSearch.VoiceWorkId into vws
//from voiceWorkSearch in vws.DefaultIfEmpty()
select new VoiceWorkQuery
{
VoiceWork = voiceWork,
EnglishVoiceWork = englishVoiceWork,
Circle = circle
};
}
protected override IQueryable<VoiceWorkQuery> ApplyFilters(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
{
IQueryable<VoiceWorkQuery> filteredQuery = query;
filteredQuery = ApplyKeywordsFilter(filteredQuery, criteria);
switch (criteria.SaleStatus)
{
case SaleStatus.Available:
filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate != null);
break;
case SaleStatus.Upcoming:
filteredQuery = filteredQuery.Where(x => x.VoiceWork.ExpectedDate != null);
break;
}
if (criteria.SupportedLanguages.Length > 0)
filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.Contains((Language)x.VoiceWork.SubtitleLanguage));
filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria);
filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria);
filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria);
filteredQuery = ApplyTagIdsFilter(filteredQuery, criteria);
filteredQuery = ApplyCreatorIdsFilter(filteredQuery, criteria);
if (criteria.Status is not null)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.Status == (byte)criteria.Status);
if (criteria.ScheduledReleaseDateStart is not null)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.PlannedReleaseDate >= criteria.ScheduledReleaseDateStart.Value.ToDateTime(TimeOnly.MinValue));
if (criteria.ScheduledReleaseDateEnd is not null)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.PlannedReleaseDate <= criteria.ScheduledReleaseDateEnd.Value.ToDateTime(TimeOnly.MinValue));
if (criteria.ReleaseDateStart is not null)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate >= criteria.ReleaseDateStart.Value.ToDateTime(TimeOnly.MinValue));
if (criteria.ReleaseDateEnd is not null)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate <= criteria.ReleaseDateEnd.Value.ToDateTime(TimeOnly.MinValue));
if (criteria.AgeRatings.Length > 0)
filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.Contains((AgeRating)x.VoiceWork.Rating));
//if (criteria.SupportedLanguages.Length > 0)
// filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.Contains((Language)x.VoiceWork.SubtitleLanguage));
if (criteria.AIGenerationOptions.Length > 0)
filteredQuery = filteredQuery.Where(x => criteria.AIGenerationOptions.Contains((AIGeneration)x.VoiceWork.AIGeneration));
if (criteria.ShowFavoriteVoiceWorks)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.Favorite);
if (criteria.ShowInvalidVoiceWorks)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.IsValid != true);
if (criteria.MinDownloads is not null)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.Downloads >= criteria.MinDownloads.Value);
if (criteria.MaxDownloads is not null)
filteredQuery = filteredQuery.Where(x => x.VoiceWork.Downloads <= criteria.MaxDownloads.Value);
return filteredQuery;
}
private IQueryable<VoiceWorkQuery> ApplyKeywordsFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
{
if (string.IsNullOrWhiteSpace(criteria.Keywords))
return query;
var voiceWorkIds = fullTextSearch.MatchingIds(context, criteria.Keywords);
return query.Where(x => voiceWorkIds.Contains(x.VoiceWork.VoiceWorkId));
}
private IQueryable<VoiceWorkQuery> ApplyCircleStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
{
if (criteria.CircleStatus is null)
return query;
switch (criteria.CircleStatus)
{
case CircleStatus.NotBlacklisted:
query = query.Where(q =>
!context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Blacklisted));
break;
case CircleStatus.Blacklisted:
query = query.Where(q =>
context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Blacklisted));
break;
case CircleStatus.Favorited:
query = query.Where(q =>
context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Favorite));
break;
}
//switch (criteria.CircleStatus)
//{
// case CircleStatus.NotBlacklisted:
// query = query.Where(x => x.Circle.Blacklisted == false);
// break;
// case CircleStatus.Favorited:
// query = query.Where(x => x.Circle.Favorite);
// break;
// case CircleStatus.Blacklisted:
// query = query.Where(x => x.Circle.Blacklisted);
// break;
//}
return query;
}
private IQueryable<VoiceWorkQuery> ApplyTagStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
{
if (criteria.TagStatus is null)
return query;
// Handy local predicates that translate to EXISTS subqueries
bool HasFav(int voiceWorkId) =>
context.VoiceWorkTags
.Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
.Any(x => x.vwt.VoiceWorkId == voiceWorkId && x.t.Favorite);
bool HasBlk(int voiceWorkId) =>
context.VoiceWorkTags
.Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
.Any(x => x.vwt.VoiceWorkId == voiceWorkId && x.t.Blacklisted);
return criteria.TagStatus switch
{
TagStatus.NotBlacklisted =>
query.Where(q => !context.VoiceWorkTags
.Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
.Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)),
TagStatus.Blacklisted =>
query.Where(q => context.VoiceWorkTags
.Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
.Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)),
TagStatus.FavoriteIncludeBlacklist =>
query.Where(q => context.VoiceWorkTags
.Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
.Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Favorite)),
TagStatus.FavoriteExcludeBlacklist =>
query.Where(q =>
context.VoiceWorkTags
.Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
.Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Favorite)
&&
!context.VoiceWorkTags
.Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
.Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)
),
_ => query
};
}
private IQueryable<VoiceWorkQuery> ApplyCreatorStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
{
if (criteria.CreatorStatus is null)
return query;
// Handy local predicates that translate to EXISTS subqueries
bool HasFav(int voiceWorkId) =>
context.VoiceWorkCreators
.Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
.Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Favorite);
bool HasBlk(int voiceWorkId) =>
context.VoiceWorkCreators
.Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
.Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Blacklisted);
return criteria.CreatorStatus switch
{
CreatorStatus.NotBlacklisted =>
query.Where(q => !context.VoiceWorkCreators
.Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
.Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)),
CreatorStatus.Blacklisted =>
query.Where(q => context.VoiceWorkCreators
.Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
.Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)),
CreatorStatus.FavoriteIncludeBlacklist =>
query.Where(q => context.VoiceWorkCreators
.Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
.Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite)),
CreatorStatus.FavoriteExcludeBlacklist =>
query.Where(q =>
context.VoiceWorkCreators
.Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
.Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite)
&&
!context.VoiceWorkCreators
.Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
.Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)
),
_ => query
};
}
private IQueryable<VoiceWorkQuery> ApplyTagIdsFilter(IQueryable<VoiceWorkQuery> filteredQuery, VoiceWorkSearchCriteria criteria)
{
if (criteria.TagIds.Length == 0)
return filteredQuery;
if (criteria.IncludeAllTags == false)
{
var tagQuery =
from voiceWorkTag in context.VoiceWorkTags.AsNoTracking()
where criteria.TagIds.Contains(voiceWorkTag.TagId)
select new { voiceWorkTag };
var finalTagQuery = tagQuery.Select(x => x.voiceWorkTag.VoiceWorkId);
filteredQuery = filteredQuery.Where(x => finalTagQuery.Contains(x.VoiceWork.VoiceWorkId));
}
else
{
foreach (int tagId in criteria.TagIds)
{
var tagIdQuery =
from voiceWorkTag in context.VoiceWorkTags.AsNoTracking()
where voiceWorkTag.TagId == tagId
select voiceWorkTag.VoiceWorkId;
filteredQuery =
from query in filteredQuery
join voiceWorkId in tagIdQuery on query.VoiceWork.VoiceWorkId equals voiceWorkId
select new VoiceWorkQuery
{
VoiceWork = query.VoiceWork,
EnglishVoiceWork = query.EnglishVoiceWork,
Circle = query.Circle
};
}
}
return filteredQuery;
}
private IQueryable<VoiceWorkQuery> ApplyCreatorIdsFilter(IQueryable<VoiceWorkQuery> filteredQuery, VoiceWorkSearchCriteria criteria)
{
if (criteria.CreatorIds.Length == 0)
return filteredQuery;
if (criteria.IncludeAllCreators == false)
{
var creatorQuery =
from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking()
where criteria.CreatorIds.Contains(voiceWorkCreator.CreatorId)
select new { voiceWorkCreator };
var finalCreatorQuery = creatorQuery.Select(x => x.voiceWorkCreator.VoiceWorkId);
filteredQuery = filteredQuery.Where(x => finalCreatorQuery.Contains(x.VoiceWork.VoiceWorkId));
}
else
{
foreach (int creatorId in criteria.CreatorIds)
{
var creatorIdQuery =
from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking()
where voiceWorkCreator.CreatorId == creatorId
select voiceWorkCreator.VoiceWorkId;
filteredQuery =
from query in filteredQuery
join voiceWorkId in creatorIdQuery on query.VoiceWork.VoiceWorkId equals voiceWorkId
select new VoiceWorkQuery
{
VoiceWork = query.VoiceWork,
EnglishVoiceWork = query.EnglishVoiceWork,
Circle = query.Circle
};
}
}
return filteredQuery;
}
protected override IEnumerable<(Expression<Func<VoiceWorkQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
{
yield return (x => x.VoiceWork.ProductId, SortDirection.Descending);
}
protected override Expression<Func<VoiceWorkQuery, object?>> GetSortExpression(VoiceWorkSortField field) => field switch
{
VoiceWorkSortField.ExpectedReleaseDate => x => x.VoiceWork.ExpectedDate,
VoiceWorkSortField.ScheduledReleaseDate => x => x.VoiceWork.PlannedReleaseDate,
VoiceWorkSortField.ReleaseDate => x => x.VoiceWork.SalesDate,
VoiceWorkSortField.AnyReleaseDate => x => x.VoiceWork.SalesDate ?? x.VoiceWork.PlannedReleaseDate ?? x.VoiceWork.ExpectedDate,
VoiceWorkSortField.Downloads => x => x.VoiceWork.Downloads ?? 0,
VoiceWorkSortField.WishlistCount => x => x.VoiceWork.WishlistCount ?? 0,
VoiceWorkSortField.StarRating => x => x.VoiceWork.StarRating ?? 0,
VoiceWorkSortField.FavoriteCircle => x => !x.Circle.Favorite,
_ => x => x.VoiceWork.ProductId
};
protected override IQueryable<int> GetSelectIdQuery(IOrderedQueryable<VoiceWorkQuery> query)
{
return query.Select(x => x.VoiceWork.VoiceWorkId);
}
protected override IQueryable<VoiceWorkSearchResult> GetSelectQuery(IOrderedQueryable<VoiceWorkQuery> query)
{
var result =
from q in query
let voiceWork = q.VoiceWork
let englishVoiceWork = q.EnglishVoiceWork
let circle = q.Circle
let productLinkPage = voiceWork.SalesDate.HasValue ? "work" : "announce"
select new VoiceWorkSearchResult()
{
VoiceWorkId = voiceWork.VoiceWorkId,
ProductId = voiceWork.ProductId,
OriginalProductId = voiceWork.OriginalProductId,
ProductName = englishVoiceWork != null ? englishVoiceWork.ProductName : voiceWork.ProductName,
ProductUrl = "http://www.dlsite.com/maniax/" + productLinkPage + "/=/product_id/" + voiceWork.ProductId + ".html",
Description = englishVoiceWork != null ? englishVoiceWork.Description : voiceWork.Description,
Favorite = voiceWork.Favorite,
HasImage = voiceWork.HasImage,
Maker = circle.Name,
MakerId = circle.MakerId,
ExpectedDate = voiceWork.ExpectedDate,
SalesDate = voiceWork.SalesDate,
PlannedReleaseDate = voiceWork.PlannedReleaseDate,
Downloads = voiceWork.Downloads,
WishlistCount = voiceWork.WishlistCount,
Status = voiceWork.Status,
SubtitleLanguage = voiceWork.SubtitleLanguage,
HasTrial = voiceWork.HasTrial,
HasChobit = voiceWork.HasChobit,
IsValid = voiceWork.IsValid
};
return result;
}
protected override async Task<Dictionary<int, VoiceWorkSearchResult>> GetItems(int[] ids)
{
var result =
from voiceWork in context.VoiceWorks.AsNoTracking()
join englishVoiceWork in context.EnglishVoiceWorks.AsNoTracking() on voiceWork.VoiceWorkId equals englishVoiceWork.VoiceWorkId into ps
from englishVoiceWork in ps.DefaultIfEmpty()
join circle in context.Circles.AsNoTracking() on voiceWork.CircleId equals circle.CircleId into cs
from circle in cs.DefaultIfEmpty()
let productLinkPage = voiceWork.SalesDate.HasValue ? "work" : "announce"
where ids.Contains(voiceWork.VoiceWorkId)
select new VoiceWorkSearchResult()
{
VoiceWorkId = voiceWork.VoiceWorkId,
ProductId = voiceWork.ProductId,
OriginalProductId = voiceWork.OriginalProductId,
ProductName = englishVoiceWork != null ? englishVoiceWork.ProductName : voiceWork.ProductName,
ProductUrl = "http://www.dlsite.com/maniax/" + productLinkPage + "/=/product_id/" + voiceWork.ProductId + ".html",
Description = englishVoiceWork != null ? englishVoiceWork.Description : voiceWork.Description,
Favorite = voiceWork.Favorite,
HasImage = voiceWork.HasImage,
Maker = circle.Name,
MakerId = circle.MakerId,
ExpectedDate = voiceWork.ExpectedDate,
SalesDate = voiceWork.SalesDate,
PlannedReleaseDate = voiceWork.PlannedReleaseDate,
Downloads = voiceWork.Downloads,
WishlistCount = voiceWork.WishlistCount,
Status = voiceWork.Status,
SubtitleLanguage = voiceWork.SubtitleLanguage,
HasTrial = voiceWork.HasTrial,
HasChobit = voiceWork.HasChobit,
IsValid = voiceWork.IsValid
};
return await result.ToDictionaryAsync(x => x.VoiceWorkId);
}
protected override async Task PostLoadAsync(IList<VoiceWorkSearchResult> items, CancellationToken cancellationToken)
{
if (items.Count == 0)
return;
int[] voiceWorkIds = [.. items.Select(i => i.VoiceWorkId)];
Dictionary<int, VoiceWorkTagItem[]> tagsByVw = await GetTagsAsync(voiceWorkIds, cancellationToken);
Dictionary<int, VoiceWorkCreatorItem[]> creatorsByVw = await GetCreatorsAsync(voiceWorkIds, cancellationToken);
foreach (VoiceWorkSearchResult item in items)
{
if (tagsByVw.TryGetValue(item.VoiceWorkId, out VoiceWorkTagItem[]? tags))
item.Tags = tags;
if (creatorsByVw.TryGetValue(item.VoiceWorkId, out VoiceWorkCreatorItem[]? creators))
item.Creators = creators;
}
}
private async Task<Dictionary<int, VoiceWorkTagItem[]>> GetTagsAsync(int[] voiceWorkIds, CancellationToken cancellationToken)
{
var tagRows = await (
from voiceWorkTag in context.VoiceWorkTags.AsNoTracking()
join tag in context.Tags.AsNoTracking() on voiceWorkTag.TagId equals tag.TagId
join englishTag in context.EnglishTags.AsNoTracking() on tag.TagId equals englishTag.TagId into et
from englishTag in et.DefaultIfEmpty()
where voiceWorkIds.Contains(voiceWorkTag.VoiceWorkId)
orderby voiceWorkTag.VoiceWorkId, voiceWorkTag.Position
select new { voiceWorkTag.VoiceWorkId, voiceWorkTag.TagId, tag.Name, EnglishName = englishTag.Name }
).ToListAsync(cancellationToken);
return tagRows
.GroupBy(r => r.VoiceWorkId)
.ToDictionary(
g => g.Key,
g => g.Select(r => new VoiceWorkTagItem { TagId = r.TagId, Name = r.EnglishName ?? r.Name }).ToArray()
);
}
private async Task<Dictionary<int, VoiceWorkCreatorItem[]>> GetCreatorsAsync(int[] voiceWorkIds, CancellationToken cancellationToken)
{
var creatorRows = await (
from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking()
join creator in context.Creators.AsNoTracking() on voiceWorkCreator.CreatorId equals creator.CreatorId
where voiceWorkIds.Contains(voiceWorkCreator.VoiceWorkId)
orderby voiceWorkCreator.VoiceWorkId, voiceWorkCreator.Position
select new { voiceWorkCreator.VoiceWorkId, creator.CreatorId, creator.Name }
).ToListAsync(cancellationToken);
return creatorRows
.GroupBy(r => r.VoiceWorkId)
.ToDictionary(
g => g.Key,
g => g.Select(r => new VoiceWorkCreatorItem { CreatorId = r.CreatorId, Name = r.Name }).ToArray()
);
}
}